Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
791927e45e | ||
|
|
fa984e1568 | ||
|
|
9b197da5be | ||
|
|
b7c316b01b | ||
|
|
5ab56310bf | ||
|
|
52e318ca18 | ||
|
|
8d078564dd | ||
|
|
7b5482e83d | ||
|
|
23086476b3 | ||
|
|
00cab5c41f | ||
|
|
fbc27e1d52 | ||
|
|
e1c9a8cbfc | ||
|
|
8ee109e975 | ||
|
|
cfaecc3e02 | ||
|
|
18246e0dc1 | ||
|
|
6dcb16724c | ||
|
|
d4c5a906b5 | ||
|
|
27e8d73b82 | ||
|
|
25eae4a546 | ||
|
|
f663774fee | ||
|
|
e2258fba27 | ||
|
|
d0d8878f34 | ||
|
|
59ff6c35ee | ||
|
|
3c07e305ce | ||
|
|
b5f274d663 | ||
|
|
6c3ae984fc | ||
|
|
a2e9bf0504 | ||
|
|
8aa089efe6 | ||
|
|
de64d3d9d4 | ||
|
|
71833c0737 | ||
|
|
c78f4deaf7 | ||
|
|
24fc7d611f | ||
|
|
8afbc3b457 | ||
|
|
da18aae21c | ||
|
|
87a0d533ef | ||
|
|
7cdd453953 | ||
|
|
8143816c18 | ||
|
|
8243ac3c00 | ||
|
|
207e120d66 | ||
|
|
b25d04da1c | ||
|
|
7064acf756 | ||
|
|
716407ad90 | ||
|
|
af0b08bb2b | ||
|
|
3e27735ccb | ||
|
|
76c087f29c | ||
|
|
e05bcf98c2 | ||
|
|
636e88d467 | ||
|
|
34a61214d3 | ||
|
|
b1af30e026 | ||
|
|
870b59d617 | ||
|
|
e25094ccab | ||
|
|
1376f0991f | ||
|
|
443329b99f | ||
|
|
a1fc4ec26d | ||
|
|
d462298e79 | ||
|
|
965cff38c2 | ||
|
|
f96e909dce | ||
|
|
a4d6f7da62 | ||
|
|
ce9413e0dd | ||
|
|
8fc0dfb763 | ||
|
|
690edcf0a7 | ||
|
|
b7dbf10068 | ||
|
|
83b4ba94fc | ||
|
|
62a09e7098 | ||
|
|
d2c4e2894d | ||
|
|
070d314897 | ||
|
|
0a76368e98 | ||
|
|
99287d9eb4 | ||
|
|
100035cb89 | ||
|
|
1750dbead6 | ||
|
|
4c8ed3d11f | ||
|
|
f21c4eb11e | ||
|
|
3f71a41d7e | ||
|
|
1ba191c4ed | ||
|
|
6414903567 | ||
|
|
e62d8b4596 | ||
|
|
e27b1b1bc2 | ||
|
|
9507138f29 | ||
|
|
e5fa202a09 | ||
|
|
447098bc97 | ||
|
|
dc5e042cdd | ||
|
|
92fb365b54 | ||
|
|
161f5c7000 | ||
|
|
c8744768c4 | ||
|
|
b34a568a93 | ||
|
|
a669403604 | ||
|
|
7b2bfc2a64 | ||
|
|
7655c0c867 | ||
|
|
6fbabb4b26 | ||
|
|
edfc9ff082 | ||
|
|
59a9a77e3f | ||
|
|
8e756d7648 | ||
|
|
5763c1c2f1 | ||
|
|
6c5ae0b4d9 | ||
|
|
9cb9ce2e42 | ||
|
|
018be43017 | ||
|
|
4f720c8c9b | ||
|
|
06e1e113f0 | ||
|
|
3fc6f38da9 | ||
|
|
8e8c96c267 | ||
|
|
fab765c5ee | ||
|
|
89ed07a097 | ||
|
|
d7d73d2caf | ||
|
|
aa92730e28 | ||
|
|
430430e0cd | ||
|
|
62713940b0 | ||
|
|
75aef09338 | ||
|
|
4e65c30e18 | ||
|
|
8b873e2828 | ||
|
|
e70d179628 | ||
|
|
ca9a836300 | ||
|
|
abdf72dc0f | ||
|
|
227cd165f6 | ||
|
|
a0135cdc59 | ||
|
|
112d504148 | ||
|
|
d275930732 | ||
|
|
97e33bdd7e | ||
|
|
3f13791faf | ||
|
|
359cfc4be0 | ||
|
|
1ad6bba0ab | ||
|
|
6dc4211d81 | ||
|
|
e5f0452728 | ||
|
|
d95bba63f4 | ||
|
|
6f48869021 | ||
|
|
845bfae887 | ||
|
|
3fe02fba26 | ||
|
|
4ca5ca9fe2 | ||
|
|
364e187080 | ||
|
|
b1193eef1e | ||
|
|
e110f12cf1 | ||
|
|
022b513935 | ||
|
|
010f39a7a4 | ||
|
|
6b6eed66eb | ||
|
|
c214e01f03 | ||
|
|
a28ac3634d | ||
|
|
a0709fac88 | ||
|
|
e5b6a3332e | ||
|
|
979686920a | ||
|
|
793850f3aa | ||
|
|
fa9d72fc0a | ||
|
|
0d485ffed6 | ||
|
|
9a2e55d528 | ||
|
|
81ecbb86a5 | ||
|
|
2cd816932b | ||
|
|
d83411162d | ||
|
|
c9c54db3a1 | ||
|
|
d100600789 |
No files matched your search
@@ -1 +0,0 @@
|
|||||||
🚀 Welcome to Cal.com! Try typing 'yarn dx' to get a quick dev envionment.
|
|
||||||
+11
-1
@@ -31,11 +31,14 @@ NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
|
|||||||
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
|
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
|
||||||
|
|
||||||
# To enable SAML login, set both these variables
|
# To enable SAML login, set both these variables
|
||||||
# @see https://github.com/calcom/cal.com/tree/main/packages/ee#setting-up-saml-login
|
# @see https://github.com/calcom/cal.com/tree/main/packages/features/ee#setting-up-saml-login
|
||||||
# SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml"
|
# SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml"
|
||||||
SAML_DATABASE_URL=
|
SAML_DATABASE_URL=
|
||||||
# SAML_ADMINS='pro@example.com'
|
# SAML_ADMINS='pro@example.com'
|
||||||
SAML_ADMINS=
|
SAML_ADMINS=
|
||||||
|
# NEXT_PUBLIC_HOSTED_CAL_FEATURES=1
|
||||||
|
NEXT_PUBLIC_HOSTED_CAL_FEATURES=
|
||||||
|
|
||||||
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
|
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
|
||||||
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
|
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
|
||||||
# PGSSLMODE='no-verify'
|
# PGSSLMODE='no-verify'
|
||||||
@@ -79,6 +82,7 @@ SEND_FEEDBACK_EMAIL=
|
|||||||
# Used for email reminders in workflows and internal sync services
|
# Used for email reminders in workflows and internal sync services
|
||||||
SENDGRID_API_KEY=
|
SENDGRID_API_KEY=
|
||||||
SENDGRID_EMAIL=
|
SENDGRID_EMAIL=
|
||||||
|
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||||
|
|
||||||
# Twilio
|
# Twilio
|
||||||
# Used to send SMS reminders in workflows
|
# Used to send SMS reminders in workflows
|
||||||
@@ -86,6 +90,7 @@ TWILIO_SID=
|
|||||||
TWILIO_TOKEN=
|
TWILIO_TOKEN=
|
||||||
TWILIO_MESSAGING_SID=
|
TWILIO_MESSAGING_SID=
|
||||||
TWILIO_PHONE_NUMBER=
|
TWILIO_PHONE_NUMBER=
|
||||||
|
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
|
||||||
NEXT_PUBLIC_SENDER_ID=
|
NEXT_PUBLIC_SENDER_ID=
|
||||||
TWILIO_VERIFY_SID=
|
TWILIO_VERIFY_SID=
|
||||||
|
|
||||||
@@ -102,6 +107,8 @@ STRIPE_TEAM_MONTHLY_PRICE_ID=
|
|||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
STRIPE_PRIVATE_KEY=
|
STRIPE_PRIVATE_KEY=
|
||||||
STRIPE_CLIENT_ID=
|
STRIPE_CLIENT_ID=
|
||||||
|
PAYMENT_FEE_FIXED=
|
||||||
|
PAYMENT_FEE_PERCENTAGE=
|
||||||
|
|
||||||
# Use for internal Public API Keys and optional
|
# Use for internal Public API Keys and optional
|
||||||
API_KEY_PREFIX=cal_
|
API_KEY_PREFIX=cal_
|
||||||
@@ -147,3 +154,6 @@ SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
|||||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||||
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
|
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
|
||||||
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
||||||
|
# Set this to true in to disable new signups
|
||||||
|
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
||||||
|
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
"♻️ autoupdate":
|
|
||||||
- apps/web/public/static/locales/**/*.json
|
|
||||||
|
|
||||||
automerge:
|
|
||||||
- apps/web/public/static/locales/**/*.json
|
|
||||||
|
|
||||||
"❗️ migrations":
|
"❗️ migrations":
|
||||||
- packages/prisma/migrations/**/migration.sql
|
- packages/prisma/migrations/**/migration.sql
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
required-checks:
|
||||||
|
uses: calcom/cal.com/.github/workflows/required-checks.yml@main
|
||||||
synchronize-with-crowdin:
|
synchronize-with-crowdin:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
upload_translations: true
|
upload_translations: true
|
||||||
download_translations: true
|
download_translations: true
|
||||||
|
pull_request_labels: "♻️ autoupdate, automerge"
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- apps/web/**
|
- apps/web/**
|
||||||
- packages/embeds/**
|
- packages/embeds/**
|
||||||
|
workflow_call:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
name: E2E test
|
name: E2E test
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
|
||||||
pull_request_target: # So we can test on forks
|
pull_request_target: # So we can test on forks
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- apps/web/**
|
- apps/web/**
|
||||||
- packages/**
|
- packages/**
|
||||||
|
workflow_call:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
@@ -14,3 +14,5 @@ jobs:
|
|||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v4
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||||
|
sync-labels: ""
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
name: "Meta Workflow: Require Conditional Status Checks"
|
name: "Meta Workflow: Require Conditional Status Checks"
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
workflow_call:
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
name: Unit tests
|
name: Unit tests
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [ feat/routing-forms/reporting ] # TODO: Remove this after merged in main
|
|
||||||
pull_request_target: # So we can test on forks
|
pull_request_target: # So we can test on forks
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- apps/web/**
|
- apps/web/**
|
||||||
- packages/**
|
- packages/**
|
||||||
|
workflow_run:
|
||||||
|
workflows: [Crowdin Action]
|
||||||
|
types: [completed]
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
@@ -80,3 +80,9 @@ apps/storybook/build-storybook.log
|
|||||||
# Snaplet
|
# Snaplet
|
||||||
.snaplet/snapshots
|
.snaplet/snapshots
|
||||||
.snaplet/structure.d.ts
|
.snaplet/structure.d.ts
|
||||||
|
|
||||||
|
# Submodules
|
||||||
|
.gitmodules
|
||||||
|
apps/api
|
||||||
|
apps/website
|
||||||
|
apps/console
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
[submodule "apps/console"]
|
|
||||||
path = apps/console
|
|
||||||
url = https://github.com/calcom/console.git
|
|
||||||
branch = main
|
|
||||||
[submodule "apps/api"]
|
|
||||||
path = apps/api
|
|
||||||
url = https://github.com/calcom/api.git
|
|
||||||
branch = main
|
|
||||||
[submodule "apps/website"]
|
|
||||||
path = apps/website
|
|
||||||
url = https://github.com/calcom/website.git
|
|
||||||
branch = main
|
|
||||||
@@ -305,6 +305,12 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
|
|||||||
|
|
||||||
See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases.
|
See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases.
|
||||||
|
|
||||||
|
<!-- RORADMAP -->
|
||||||
|
|
||||||
|
## Repo Activity
|
||||||
|
|
||||||
|
<img width="100%" src="https://repobeats.axiom.co/api/embed/6bfca2f20f39738048b6e70ca205efde46352c3d.svg" />
|
||||||
|
|
||||||
<!-- CONTRIBUTING -->
|
<!-- CONTRIBUTING -->
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
@@ -315,6 +321,17 @@ Please see our [contributing guide](/CONTRIBUTING.md).
|
|||||||
|
|
||||||
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
||||||
|
|
||||||
|
|
||||||
|
<!-- TRANSLATIONS -->
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
Don't code but still want to contribute? Join our [slack](https://cal.com/slack) and join the [#i18n channel](https://calendso.slack.com/archives/C02BY67GMMW) and let us know what language you want to translate.
|
||||||
|
|
||||||
|
                       and click Get API Keys.
|
|
||||||
1. Create a team with the team name you desire
|
|
||||||
1. Head to the configuration section on the sidebar of the dashboard
|
|
||||||
1. Click on API keys and you'll find your sandbox `api_key`.
|
|
||||||
1. Copy your `api_key` to `VITAL_API_KEY` in the .env.appStore file.
|
|
||||||
1. Open [Vital Webhooks](https://app.tryvital.io/team/{team_id}/webhooks) and add `<CALCOM BASE URL>/api/integrations/vital/webhook` as webhook for connected applications.
|
|
||||||
1. Select all events for the webhook you interested, e.g. `sleep_created`
|
|
||||||
1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file.
|
|
||||||
|
|
||||||
## Workflows
|
## Workflows
|
||||||
|
|
||||||
### Setting up SendGrid for Email reminders
|
### Setting up SendGrid for Email reminders
|
||||||
@@ -414,6 +420,7 @@ following
|
|||||||
3. Copy API key to your .env file into the SENDGRID_API_KEY field
|
3. Copy API key to your .env file into the SENDGRID_API_KEY field
|
||||||
4. Go to Settings -> Sender Authentication and verify a single sender
|
4. Go to Settings -> Sender Authentication and verify a single sender
|
||||||
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
|
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
|
||||||
|
6. Add your custom sender name to the .env file into the NEXT_PUBLIC_SENDGRID_SENDER_NAME field (fallback is Cal.com)
|
||||||
|
|
||||||
### Setting up Twilio for SMS reminders
|
### Setting up Twilio for SMS reminders
|
||||||
|
|
||||||
|
|||||||
-1
Submodule apps/api deleted from 9c23a8e5ab.
-1
Submodule apps/console deleted from 2d577dd9ed.
@@ -17,7 +17,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iframe-resizer-react": "^1.1.0",
|
"iframe-resizer-react": "^1.1.0",
|
||||||
"next": "^12.3.1",
|
"next": "^13.1.1",
|
||||||
"nextra": "^1.1.0",
|
"nextra": "^1.1.0",
|
||||||
"nextra-theme-docs": "^1.2.2",
|
"nextra-theme-docs": "^1.2.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ All apps can be found under `packages/app-store`. In this folder is `_example` w
|
|||||||
| ├──index.ts
|
| ├──index.ts
|
||||||
| ├──package.json
|
| ├──package.json
|
||||||
| ├──.env.example
|
| ├──.env.example
|
||||||
| ├──README.mdx
|
| ├──DESCRIPTION.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
@@ -48,9 +48,9 @@ Under the `/components` folder, this is where the install button for your app sh
|
|||||||
|
|
||||||
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
|
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
|
||||||
|
|
||||||
On the app store page you can customize your apps description by adding a markdown file called `README.mdx`. If you do not add one then the description from you `package.json` will be used instead.
|
On the app store page you can customize your apps description by adding a markdown file called `DESCRIPTION.md`. If you do not add one then the description from you `package.json` will be used instead.
|
||||||
|
|
||||||
The `/static` folder is where you can store your app icon and any images that your `README.mdx` may use.
|
The `/static` folder is where you can store your app icon and any images that your `DESCRIPTION.md` may use.
|
||||||
|
|
||||||
## Adding Your App to the App Store
|
## Adding Your App to the App Store
|
||||||
To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object.
|
To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
storybook-static
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
@@ -21,4 +22,4 @@ dist-ssr
|
|||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
@@ -11,13 +11,15 @@ module.exports = {
|
|||||||
"@storybook/addon-links",
|
"@storybook/addon-links",
|
||||||
"@storybook/addon-essentials",
|
"@storybook/addon-essentials",
|
||||||
"@storybook/addon-interactions",
|
"@storybook/addon-interactions",
|
||||||
|
"storybook-addon-rtl-direction",
|
||||||
"storybook-react-i18next",
|
"storybook-react-i18next",
|
||||||
{
|
"storybook-addon-next",
|
||||||
|
/*{
|
||||||
name: "storybook-addon-next",
|
name: "storybook-addon-next",
|
||||||
options: {
|
options: {
|
||||||
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
|
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
|
||||||
},
|
},
|
||||||
},
|
},*/
|
||||||
],
|
],
|
||||||
framework: "@storybook/react",
|
framework: "@storybook/react",
|
||||||
core: {
|
core: {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
const withBundleAnalyzer = require("@next/bundle-analyzer");
|
||||||
|
|
||||||
|
const withTM = require("next-transpile-modules")([
|
||||||
|
"@calcom/app-store",
|
||||||
|
"@calcom/dayjs",
|
||||||
|
"@calcom/emails",
|
||||||
|
"@calcom/trpc",
|
||||||
|
"@calcom/embed-core",
|
||||||
|
"@calcom/embed-react",
|
||||||
|
"@calcom/features",
|
||||||
|
"@calcom/lib",
|
||||||
|
"@calcom/prisma",
|
||||||
|
"@calcom/ui",
|
||||||
|
]);
|
||||||
|
const glob = require("glob");
|
||||||
|
|
||||||
|
const plugins = [];
|
||||||
|
plugins.push(withTM, withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
|
||||||
|
|
||||||
|
/** @type {import("next").NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
images: {
|
||||||
|
domains: ["www.datocms-assets.com"],
|
||||||
|
formats: ["image/avif", "image/webp"],
|
||||||
|
},
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
experimental: { images: { allowFutureImage: true } },
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
|
webpack: (config, { isServer }) => {
|
||||||
|
if (!isServer) {
|
||||||
|
// don't resolve 'fs' module on the client to prevent this error on build --> Error: Can't resolve 'fs'
|
||||||
|
config.resolve.fallback = {
|
||||||
|
fs: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
@@ -20,9 +20,10 @@
|
|||||||
"@radix-ui/react-slider": "^1.0.0",
|
"@radix-ui/react-slider": "^1.0.0",
|
||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
"next": "^12.3.1",
|
"next": "^13.1.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"storybook-addon-rtl-direction": "^0.0.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.19.6",
|
"@babel/core": "^7.19.6",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^11.6.0",
|
"highlight.js": "^11.6.0",
|
||||||
"isarray": "2.0.5",
|
"isarray": "2.0.5",
|
||||||
"next": "^12.3.1",
|
"next": "^13.1.1",
|
||||||
"openapi-snippet": "^0.13.0",
|
"openapi-snippet": "^0.13.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Badge, ListItemText } from "@calcom/ui";
|
||||||
|
|
||||||
|
interface AppListCardProps {
|
||||||
|
logo?: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
actions?: ReactNode;
|
||||||
|
isDefault?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppListCard(props: AppListCardProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { logo, title, description, actions, isDefault } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-center gap-x-3">
|
||||||
|
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
||||||
|
|
||||||
|
<div className="flex grow flex-col gap-y-1 truncate">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
||||||
|
{isDefault ? <Badge variant="green">{t("default")}</Badge> : null}
|
||||||
|
</div>
|
||||||
|
<ListItemText component="p">{description}</ListItemText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Suspense, SuspenseProps } from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper around `<Suspense />` which will render the `fallback` when on server
|
|
||||||
* Can be simply replaced by `<Suspense />` once React 18 is ready.
|
|
||||||
*/
|
|
||||||
export const ClientSuspense = (props: SuspenseProps) => {
|
|
||||||
return <>{typeof window !== "undefined" ? <Suspense {...props} /> : props.fallback}</>;
|
|
||||||
};
|
|
||||||
@@ -504,7 +504,7 @@ const tabs = [
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<small className="flex py-4 text-neutral-500">
|
<small className="flex py-4 text-gray-500">
|
||||||
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
|
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -556,7 +556,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<small className="flex py-4 text-neutral-500">{t("create_update_react_component")}</small>
|
<small className="flex py-4 text-gray-500">{t("create_update_react_component")}</small>
|
||||||
<TextArea
|
<TextArea
|
||||||
data-testid="embed-react"
|
data-testid="embed-react"
|
||||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||||
@@ -597,7 +597,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
|||||||
<iframe
|
<iframe
|
||||||
ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>}
|
ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>}
|
||||||
data-testid="embed-preview"
|
data-testid="embed-preview"
|
||||||
className="border-1 h-[100vh] border"
|
className="h-[100vh] border"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
|
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
|
||||||
@@ -639,7 +639,7 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
{embeds.map((embed, index) => (
|
{embeds.map((embed, index) => (
|
||||||
<button
|
<button
|
||||||
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100 ltr:mr-2 rtl:ml-2"
|
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
|
||||||
key={index}
|
key={index}
|
||||||
data-testid={embed.type}
|
data-testid={embed.type}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -650,7 +650,7 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
|
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
|
||||||
{embed.illustration}
|
{embed.illustration}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 font-medium text-neutral-900">{embed.title}</div>
|
<div className="mt-2 font-medium text-gray-900">{embed.title}</div>
|
||||||
<p className="text-sm text-gray-500">{embed.subtitle}</p>
|
<p className="text-sm text-gray-500">{embed.subtitle}</p>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -827,7 +827,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger
|
<CollapsibleTrigger
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center text-base font-medium text-neutral-900">
|
className="flex w-full items-center text-base font-medium text-gray-900">
|
||||||
<div>
|
<div>
|
||||||
{embedType === "inline"
|
{embedType === "inline"
|
||||||
? "Inline Embed Customization"
|
? "Inline Embed Customization"
|
||||||
@@ -838,7 +838,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<Icon.FiChevronRight
|
<Icon.FiChevronRight
|
||||||
className={`${
|
className={`${
|
||||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
||||||
} ml-auto h-5 w-5 text-neutral-500`}
|
} ml-auto h-5 w-5 text-gray-500`}
|
||||||
/>
|
/>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent className="text-sm">
|
<CollapsibleContent className="text-sm">
|
||||||
@@ -1001,11 +1001,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
open={isBookingCustomizationOpen}
|
open={isBookingCustomizationOpen}
|
||||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger className="flex w-full" type="button">
|
<CollapsibleTrigger className="flex w-full" type="button">
|
||||||
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div>
|
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
|
||||||
<Icon.FiChevronRight
|
<Icon.FiChevronRight
|
||||||
className={`${
|
className={`${
|
||||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
||||||
} ml-auto h-5 w-5 text-neutral-500`}
|
} ml-auto h-5 w-5 text-gray-500`}
|
||||||
/>
|
/>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
@@ -1163,7 +1163,7 @@ export const EmbedButton = <T extends React.ElementType>({
|
|||||||
...props
|
...props
|
||||||
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
|
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
className = classNames(className, "hidden lg:inline-flex");
|
className = classNames("hidden lg:inline-flex", className);
|
||||||
const openEmbedModal = () => {
|
const openEmbedModal = () => {
|
||||||
goto(router, {
|
goto(router, {
|
||||||
dialog: "embed",
|
dialog: "embed",
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export default function ImageUploader({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
|
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
|
||||||
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
|
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
|
||||||
<input
|
<input
|
||||||
onInput={onInputFile}
|
onInput={onInputFile}
|
||||||
type="file"
|
type="file"
|
||||||
|
|||||||
@@ -62,12 +62,12 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
const className = tab.className || "";
|
const className = tab.className || "";
|
||||||
return (
|
return (
|
||||||
<Component key={tab.name}>
|
<Component key={tab.name}>
|
||||||
<Link key={tab.name} href={href} {...linkProps}>
|
<Link key={tab.name} href={href} {...linkProps} legacyBehavior>
|
||||||
<a
|
<a
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isCurrent
|
isCurrent
|
||||||
? "border-neutral-900 text-neutral-900"
|
? "border-gray-900 text-gray-900"
|
||||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
||||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
||||||
className
|
className
|
||||||
@@ -76,7 +76,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
{tab.icon && (
|
{tab.icon && (
|
||||||
<tab.icon
|
<tab.icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
|
isCurrent ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
|
||||||
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import React, { ComponentProps } from "react";
|
||||||
|
|
||||||
import { ErrorBoundary, Icon, Shell } from "@calcom/ui";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
|
import { ErrorBoundary, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
|||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { App as AppType } from "@calcom/types/App";
|
import { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText, HeadSeo } from "@calcom/ui";
|
import { Button, Icon, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||||
|
|
||||||
const Component = ({
|
const Component = ({
|
||||||
name,
|
name,
|
||||||
@@ -33,6 +34,7 @@ const Component = ({
|
|||||||
privacy,
|
privacy,
|
||||||
isProOnly,
|
isProOnly,
|
||||||
images,
|
images,
|
||||||
|
isTemplate,
|
||||||
}: Parameters<typeof App>[0]) => {
|
}: Parameters<typeof App>[0]) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const hasImages = images && images.length > 0;
|
const hasImages = images && images.length > 0;
|
||||||
@@ -98,11 +100,18 @@ const Component = ({
|
|||||||
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
|
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-sm font-medium text-gray-600">
|
<h2 className="text-sm font-medium text-gray-600">
|
||||||
<Link href={`categories/${categories[0]}`}>
|
<Link
|
||||||
<a className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">{categories[0]}</a>
|
href={`categories/${categories[0]}`}
|
||||||
|
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
|
||||||
|
{categories[0]}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
• {t("published_by", { author })}
|
• {t("published_by", { author })}
|
||||||
</h2>
|
</h2>
|
||||||
|
{isTemplate && (
|
||||||
|
<Badge variant="red" className="mt-4">
|
||||||
|
Template - Available in Dev Environment only for testing
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
{!appCredentials.isLoading ? (
|
{!appCredentials.isLoading ? (
|
||||||
@@ -307,6 +316,7 @@ export default function App(props: {
|
|||||||
licenseRequired: AppType["licenseRequired"];
|
licenseRequired: AppType["licenseRequired"];
|
||||||
isProOnly: AppType["isProOnly"];
|
isProOnly: AppType["isProOnly"];
|
||||||
images?: string[];
|
images?: string[];
|
||||||
|
isTemplate?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import {
|
|||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
Icon,
|
Icon,
|
||||||
List,
|
List,
|
||||||
ShellSubHeading,
|
|
||||||
showToast,
|
showToast,
|
||||||
AppSkeletonLoader as SkeletonLoader,
|
AppSkeletonLoader as SkeletonLoader,
|
||||||
Switch,
|
Switch,
|
||||||
|
ShellSubHeading,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
@@ -184,9 +184,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
<div className="border-t border-gray-200">
|
<div className="border-t border-gray-200">
|
||||||
{!fromOnboarding && (
|
{!fromOnboarding && (
|
||||||
<>
|
<>
|
||||||
<p className="px-4 pt-4 text-sm text-neutral-500">
|
<p className="px-4 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
|
||||||
{t("toggle_calendars_conflict")}
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-2 p-4">
|
<ul className="space-y-2 p-4">
|
||||||
{item.calendars.map((cal) => (
|
{item.calendars.map((cal) => (
|
||||||
<CalendarSwitch
|
<CalendarSwitch
|
||||||
@@ -330,7 +328,10 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
})}
|
})}
|
||||||
description={t(`no_category_apps_description_calendar`)}
|
description={t(`no_category_apps_description_calendar`)}
|
||||||
buttonRaw={
|
buttonRaw={
|
||||||
<Button color="secondary" href="/apps/categories/calendar">
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
data-testid="connect-calendar-apps"
|
||||||
|
href="/apps/categories/calendar">
|
||||||
{t(`connect_calendar_apps`)}
|
{t(`connect_calendar_apps`)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
|
import { Badge, Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ function IntegrationListItem(props: {
|
|||||||
destination?: boolean;
|
destination?: boolean;
|
||||||
separate?: boolean;
|
separate?: boolean;
|
||||||
invalidCredential?: boolean;
|
invalidCredential?: boolean;
|
||||||
|
isTemplate?: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -50,8 +51,13 @@ function IntegrationListItem(props: {
|
|||||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
||||||
{props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />}
|
{props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />}
|
||||||
<div className="flex-grow truncate pl-2">
|
<div className="flex-grow truncate pl-2">
|
||||||
<ListItemTitle component="h3">
|
<ListItemTitle component="h3" className="flex ">
|
||||||
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
|
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
|
||||||
|
{props.isTemplate && (
|
||||||
|
<Badge variant="red" className="ml-4">
|
||||||
|
Template
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</ListItemTitle>
|
</ListItemTitle>
|
||||||
<ListItemText component="p">{props.description}</ListItemText>
|
<ListItemText component="p">{props.description}</ListItemText>
|
||||||
{/* Alert error that key stopped working. */}
|
{/* Alert error that key stopped working. */}
|
||||||
|
|||||||
@@ -2,19 +2,9 @@ import { useSession } from "next-auth/react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ComponentProps } from "react";
|
import React, { ComponentProps } from "react";
|
||||||
|
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EmptyScreen, Icon, Shell, HorizontalTabs, HorizontalTabItemProps } from "@calcom/ui";
|
import { EmptyScreen, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
const tabs: HorizontalTabItemProps[] = [
|
|
||||||
{
|
|
||||||
name: "app_store",
|
|
||||||
href: "/apps",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "installed_apps",
|
|
||||||
href: "/apps/installed",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type AppsLayoutProps = {
|
type AppsLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -30,11 +20,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
|||||||
if (session.status === "loading") return <></>;
|
if (session.status === "loading") return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest} actions={actions?.("hidden sm:block")}>
|
<Shell {...rest} actions={actions?.("block")}>
|
||||||
<div className="flex flex-col xl:flex-row">
|
<div className="flex flex-col xl:flex-row">
|
||||||
<div className="block lg:hidden">
|
|
||||||
<HorizontalTabs tabs={tabs} actions={actions?.("ml-6 mr-4 mt-3 block sm:hidden")} />
|
|
||||||
</div>
|
|
||||||
<main className="w-full">
|
<main className="w-full">
|
||||||
{emptyStore ? (
|
{emptyStore ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import React, { ComponentProps } from "react";
|
|||||||
|
|
||||||
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon, Shell } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
import type { HorizontalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabItem";
|
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||||
import type { VerticalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/VerticalTabItem";
|
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||||
{
|
{
|
||||||
@@ -44,13 +44,15 @@ export default function InstalledAppsLayout({
|
|||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
|
const variant: typeof InstalledAppVariants[number] = "payment";
|
||||||
|
|
||||||
const query = trpc.viewer.integrations.useQuery({
|
const query = trpc.viewer.integrations.useQuery({
|
||||||
variant: InstalledAppVariants.payment,
|
variant,
|
||||||
onlyInstalled: true,
|
onlyInstalled: true,
|
||||||
});
|
});
|
||||||
let actualTabs = tabs;
|
let actualTabs = tabs;
|
||||||
if (query.data?.items.length === 0) {
|
if (query.data?.items.length === 0) {
|
||||||
actualTabs = tabs.filter((tab) => tab.name !== InstalledAppVariants.payment);
|
actualTabs = tabs.filter((tab) => tab.name !== variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { useRouter } from "next/router";
|
|||||||
import React, { ComponentProps, useEffect } from "react";
|
import React, { ComponentProps, useEffect } from "react";
|
||||||
|
|
||||||
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import { ErrorBoundary, Shell } from "@calcom/ui";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|
||||||
import { UserPermissionRole } from ".prisma/client";
|
import { UserPermissionRole } from ".prisma/client";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import classNames from "@lib/classNames";
|
|||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
slug: eventTypeSlug,
|
slug: eventTypeSlug,
|
||||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||||
ethSignature,
|
...(ethSignature ? { ethSignature } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,29 +114,29 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link href={bookingUrl} prefetch={false}>
|
<Link
|
||||||
<a
|
href={bookingUrl}
|
||||||
className={classNames(
|
prefetch={false}
|
||||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
className={classNames(
|
||||||
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent dark:text-neutral-200",
|
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
|
||||||
)}
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
data-testid="time">
|
)}
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
data-testid="time">
|
||||||
{!!seatsPerTimeSlot && (
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
<p
|
{!!seatsPerTimeSlot && (
|
||||||
className={`${
|
<p
|
||||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
className={`${
|
||||||
? "text-rose-600"
|
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
? "text-rose-600"
|
||||||
? "text-yellow-500"
|
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||||
: "text-emerald-400"
|
? "text-yellow-500"
|
||||||
} text-sm`}>
|
: "text-emerald-400"
|
||||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
} text-sm`}>
|
||||||
{seatsPerTimeSlot} {t("seats_available")}
|
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||||
</p>
|
{seatsPerTimeSlot} {t("seats_available")}
|
||||||
)}
|
</p>
|
||||||
</a>
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
profile={profile}
|
profile={profile}
|
||||||
users={eventType.users}
|
users={eventType.users}
|
||||||
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
||||||
size={10}
|
size="sm"
|
||||||
truncateAfter={3}
|
truncateAfter={3}
|
||||||
/>
|
/>
|
||||||
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||||
|
|||||||
@@ -220,7 +220,8 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const showRecordingsButtons = booking.location === "integrations:daily" && isPast && isConfirmed;
|
const showRecordingsButtons =
|
||||||
|
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -273,7 +274,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row">
|
<tr className="group flex flex-col hover:bg-gray-50 sm:flex-row">
|
||||||
<td
|
<td
|
||||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||||
onClick={onClickTableData}>
|
onClick={onClickTableData}>
|
||||||
@@ -356,7 +357,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<div
|
<div
|
||||||
title={booking.title}
|
title={booking.title}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-neutral-900 md:max-w-full",
|
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
|
||||||
isCancelled ? "line-through" : ""
|
isCancelled ? "line-through" : ""
|
||||||
)}>
|
)}>
|
||||||
{booking.title}
|
{booking.title}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
placeholder={t("cancellation_reason_placeholder")}
|
placeholder={t("cancellation_reason_placeholder")}
|
||||||
value={cancellationReason}
|
value={cancellationReason}
|
||||||
onChange={(e) => setCancellationReason(e.target.value)}
|
onChange={(e) => setCancellationReason(e.target.value)}
|
||||||
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white "
|
className="dark:bg-darkgray-100 dark:border-darkgray-400 mt-2 mb-4 w-full dark:text-white "
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
|
|||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
|
import { AvatarGroup, AvatarGroupProps } from "@calcom/ui";
|
||||||
import AvatarGroup, { AvatarGroupProps } from "@components/ui/AvatarGroup";
|
|
||||||
|
|
||||||
export const UserAvatars = ({
|
export const UserAvatars = ({
|
||||||
profile,
|
profile,
|
||||||
@@ -14,7 +13,6 @@ export const UserAvatars = ({
|
|||||||
const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers;
|
const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers;
|
||||||
return (
|
return (
|
||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
border="border-2 dark:border-darkgray-100 border-white"
|
|
||||||
items={
|
items={
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -38,7 +36,7 @@ export const UserAvatars = ({
|
|||||||
href?: string;
|
href?: string;
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
size={props.size}
|
size="sm"
|
||||||
truncateAfter={props.truncateAfter}
|
truncateAfter={props.truncateAfter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ const BookingPage = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"main overflow-hidden",
|
"main overflow-hidden",
|
||||||
isBackgroundTransparent ? "" : "dark:border-1 dark:bg-darkgray-100 bg-white",
|
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
|
||||||
"dark:border-darkgray-300 rounded-md sm:border"
|
"dark:border-darkgray-300 rounded-md sm:border"
|
||||||
)}>
|
)}>
|
||||||
<div className="sm:flex">
|
<div className="sm:flex">
|
||||||
@@ -954,19 +954,14 @@ const BookingPage = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
size="icon"
|
variant="icon"
|
||||||
tooltip={t("additional_guests")}
|
tooltip={t("additional_guests")}
|
||||||
StartIcon={Icon.FiUserPlus}
|
StartIcon={Icon.FiUserPlus}
|
||||||
onClick={() => setGuestToggle(!guestToggle)}
|
onClick={() => setGuestToggle(!guestToggle)}
|
||||||
className="mr-auto"
|
className="mr-auto"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button color="minimal" type="button" onClick={() => router.back()}>
|
||||||
color="minimal"
|
|
||||||
type="button"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
// We override this for this component only for now - as we don't support darkmode everywhere in the app
|
|
||||||
className="dark:hover:bg-darkgray-200 dark:border-none dark:text-white">
|
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog}>
|
<Dialog open={isOpenDialog}>
|
||||||
<DialogContent>
|
<DialogContent disableOverflow>
|
||||||
<div className="flex flex-row space-x-3">
|
<div className="flex flex-row space-x-3">
|
||||||
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="min-w-4 mb-2">
|
<div className="min-w-4 mb-2">
|
||||||
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-neutral-700">
|
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-gray-700">
|
||||||
{t("availability")}
|
{t("availability")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ function RadioInputHandler({
|
|||||||
{...register(`options.${index}.label` as const, { required: true })}
|
{...register(`options.${index}.label` as const, { required: true })}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
variant="icon"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
StartIcon={Icon.FiX}
|
StartIcon={Icon.FiX}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -93,10 +93,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<Label>{t("add_to_calendar")}</Label>
|
<Label>{t("add_to_calendar")}</Label>
|
||||||
<Link href="/apps/categories/calendar">
|
<Link
|
||||||
<a target="_blank" className="text-sm text-gray-600 hover:text-gray-900">
|
href="/apps/categories/calendar"
|
||||||
{t("add_another_calendar")}
|
target="_blank"
|
||||||
</a>
|
className="text-sm text-gray-600 hover:text-gray-900">
|
||||||
|
{t("add_another_calendar")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="-mt-1 w-full">
|
<div className="-mt-1 w-full">
|
||||||
@@ -128,7 +129,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={Icon.FiEdit}
|
StartIcon={Icon.FiEdit}
|
||||||
size="icon"
|
variant="icon"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
||||||
onClick={() => setShowEventNameTip((old) => !old)}
|
onClick={() => setShowEventNameTip((old) => !old)}
|
||||||
@@ -266,12 +267,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
/>
|
/>
|
||||||
{/*TODO: Extract it out into a component when used more than once*/}
|
{/*TODO: Extract it out into a component when used more than once*/}
|
||||||
{!IS_SELF_HOSTED && (
|
{!IS_SELF_HOSTED && (
|
||||||
<Link href="https://cal.com/pricing">
|
<Link href="https://cal.com/pricing" target="_blank">
|
||||||
<a target="_blank">
|
<Badge variant="orange" className="ml-2">
|
||||||
<Badge variant="orange" className="ml-2">
|
Platform Only
|
||||||
Platform Only
|
</Badge>
|
||||||
</Badge>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,43 +1,17 @@
|
|||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
import { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated";
|
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||||
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||||
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui";
|
import { Button, EmptyScreen, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||||
EventTypeAppCardComponentProps["eventType"];
|
EventTypeAppCardComponentProps["eventType"];
|
||||||
|
|
||||||
function AppCardWrapper({
|
|
||||||
app,
|
|
||||||
eventType,
|
|
||||||
getAppData,
|
|
||||||
setAppData,
|
|
||||||
}: {
|
|
||||||
app: RouterOutputs["viewer"]["apps"][number];
|
|
||||||
eventType: EventType;
|
|
||||||
getAppData: GetAppData;
|
|
||||||
setAppData: SetAppData;
|
|
||||||
}) {
|
|
||||||
const dirName = app.slug === "stripe" ? "stripepayment" : app.slug;
|
|
||||||
const Component = EventTypeAddonMap[dirName as keyof typeof EventTypeAddonMap];
|
|
||||||
|
|
||||||
if (!Component) {
|
|
||||||
throw new Error('No component found for "' + dirName + '"');
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ErrorBoundary message={`There is some problem with ${app.name} App`}>
|
|
||||||
<EventTypeAppContext.Provider value={[getAppData, setAppData]}>
|
|
||||||
<Component key={app.slug} app={app} eventType={eventType} />
|
|
||||||
</EventTypeAppContext.Provider>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({
|
const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({
|
||||||
@@ -97,7 +71,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{installedApps?.map((app) => (
|
{installedApps?.map((app) => (
|
||||||
<AppCardWrapper
|
<EventTypeAppCard
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||||
key={app.slug}
|
key={app.slug}
|
||||||
@@ -113,7 +87,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="before:border-0">
|
<div className="before:border-0">
|
||||||
{notInstalledApps?.map((app) => (
|
{notInstalledApps?.map((app) => (
|
||||||
<AppCardWrapper
|
<EventTypeAppCard
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||||
key={app.slug}
|
key={app.slug}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext, UseFormRegisterReturn, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
|
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||||
@@ -12,13 +12,95 @@ import { PeriodType } from "@calcom/prisma/client";
|
|||||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||||
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||||
|
|
||||||
|
const MinimumBookingNoticeInput = React.forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
Omit<UseFormRegisterReturn<"minimumBookingNotice">, "ref">
|
||||||
|
>(function MinimumBookingNoticeInput({ ...passThroughProps }, ref) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { setValue, getValues } = useFormContext<FormValues>();
|
||||||
|
const durationTypeOptions: {
|
||||||
|
value: DurationType;
|
||||||
|
label: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
label: t("minutes"),
|
||||||
|
value: "minutes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("hours"),
|
||||||
|
value: "hours",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("days"),
|
||||||
|
value: "days",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [minimumBookingNoticeDisplayValues, setMinimumBookingNoticeDisplayValues] = useState<{
|
||||||
|
type: DurationType;
|
||||||
|
value: number;
|
||||||
|
}>({
|
||||||
|
type: findDurationType(getValues(passThroughProps.name)),
|
||||||
|
value: convertToNewDurationType(
|
||||||
|
"minutes",
|
||||||
|
findDurationType(getValues(passThroughProps.name)),
|
||||||
|
getValues(passThroughProps.name)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
// keep hidden field in sync with minimumBookingNoticeDisplayValues
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(
|
||||||
|
passThroughProps.name,
|
||||||
|
convertToNewDurationType(
|
||||||
|
minimumBookingNoticeDisplayValues.type,
|
||||||
|
"minutes",
|
||||||
|
minimumBookingNoticeDisplayValues.value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-end justify-end">
|
||||||
|
<div className="w-1/2 md:w-full">
|
||||||
|
<InputField
|
||||||
|
required
|
||||||
|
defaultValue={minimumBookingNoticeDisplayValues.value}
|
||||||
|
onChange={(e) =>
|
||||||
|
setMinimumBookingNoticeDisplayValues({
|
||||||
|
...minimumBookingNoticeDisplayValues,
|
||||||
|
value: parseInt(e.target.value || "0", 10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
label={t("minimum_booking_notice")}
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
||||||
|
/>
|
||||||
|
<input type="hidden" ref={ref} {...passThroughProps} />
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
isSearchable={false}
|
||||||
|
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
||||||
|
defaultValue={durationTypeOptions.find(
|
||||||
|
(option) => option.value === minimumBookingNoticeDisplayValues.type
|
||||||
|
)}
|
||||||
|
onChange={(input) => {
|
||||||
|
if (input) {
|
||||||
|
setMinimumBookingNoticeDisplayValues({
|
||||||
|
...minimumBookingNoticeDisplayValues,
|
||||||
|
type: input.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={durationTypeOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const minimumBookingNoticeType = useRef<DurationType>(findDurationType(eventType.minimumBookingNotice));
|
|
||||||
const prevBookingNoticeType = useRef<DurationType>(minimumBookingNoticeType.current);
|
|
||||||
|
|
||||||
const minimumBookingNoticeInDurationTypeFormValue = formMethods.watch("minimumBookingNoticeInDurationType");
|
|
||||||
|
|
||||||
const PERIOD_TYPES = [
|
const PERIOD_TYPES = [
|
||||||
{
|
{
|
||||||
@@ -49,212 +131,115 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
defaultValue: periodType?.type,
|
defaultValue: periodType?.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
const onMinimumNoticeDurationTypeChange = useMemo(
|
|
||||||
() => (durationType?: DurationType) => {
|
|
||||||
if (typeof durationType === "undefined") return;
|
|
||||||
|
|
||||||
// Store current selected type in ref to use in previous run for comparrison.
|
|
||||||
minimumBookingNoticeType.current = durationType;
|
|
||||||
|
|
||||||
// Uses the previous selected type and converts the value to the new type.
|
|
||||||
// Eg day was selected before, user selects hours now, so we multiple * 24 and round up.
|
|
||||||
const minimumBookingNoticeInDurationType = convertToNewDurationType(
|
|
||||||
prevBookingNoticeType.current,
|
|
||||||
durationType,
|
|
||||||
Number(minimumBookingNoticeInDurationTypeFormValue)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Uses the round up value in the new type to calculate the value in minutes.
|
|
||||||
// We should NOT use minimumBookingNoticeInDurationTypeFormValue here, since that
|
|
||||||
// would break in the case that the user enters 90 minutes, changes the type to
|
|
||||||
// hours (that will show 2 hours), but the value in minutes would then remain 90,
|
|
||||||
// which is something else than the user things they save.
|
|
||||||
const minimumBookingNoticeInMinutes = convertToNewDurationType(
|
|
||||||
durationType,
|
|
||||||
"minutes",
|
|
||||||
Number(minimumBookingNoticeInDurationType)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Updates both form values as well as stores the current type as previous type in ref.
|
|
||||||
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
|
|
||||||
formMethods.setValue("minimumBookingNoticeInDurationType", minimumBookingNoticeInDurationType);
|
|
||||||
prevBookingNoticeType.current = durationType;
|
|
||||||
},
|
|
||||||
[formMethods, minimumBookingNoticeInDurationTypeFormValue]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the user inputs a new value for minimumBookingNoticeInDurationType,
|
|
||||||
* we calculate the value in minutes, and update this hidden field as well.
|
|
||||||
*/
|
|
||||||
const onMinimumNoticeChange = useMemo(
|
|
||||||
() => (duration: string) => {
|
|
||||||
const minimumBookingNoticeInMinutes = convertToNewDurationType(
|
|
||||||
minimumBookingNoticeType.current,
|
|
||||||
"minutes",
|
|
||||||
Number(duration)
|
|
||||||
);
|
|
||||||
|
|
||||||
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
|
|
||||||
},
|
|
||||||
[formMethods]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-8">
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
<div className="space-y-4 lg:space-y-8">
|
||||||
<div className="w-full">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||||
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
|
<div className="w-full">
|
||||||
<Controller
|
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
|
||||||
name="beforeBufferTime"
|
<Controller
|
||||||
control={formMethods.control}
|
name="beforeBufferTime"
|
||||||
defaultValue={eventType.beforeEventBuffer || 0}
|
control={formMethods.control}
|
||||||
render={({ field: { onChange, value } }) => {
|
defaultValue={eventType.beforeEventBuffer || 0}
|
||||||
const beforeBufferOptions = [
|
render={({ field: { onChange, value } }) => {
|
||||||
{
|
const beforeBufferOptions = [
|
||||||
label: t("event_buffer_default"),
|
{
|
||||||
value: 0,
|
label: t("event_buffer_default"),
|
||||||
},
|
value: 0,
|
||||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
},
|
||||||
label: minutes + " " + t("minutes"),
|
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||||
value: minutes,
|
label: minutes + " " + t("minutes"),
|
||||||
})),
|
value: minutes,
|
||||||
];
|
})),
|
||||||
return (
|
];
|
||||||
<Select
|
return (
|
||||||
isSearchable={false}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (val) onChange(val.value);
|
|
||||||
}}
|
|
||||||
defaultValue={
|
|
||||||
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
|
|
||||||
}
|
|
||||||
options={beforeBufferOptions}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
|
|
||||||
<Controller
|
|
||||||
name="afterBufferTime"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.afterEventBuffer || 0}
|
|
||||||
render={({ field: { onChange, value } }) => {
|
|
||||||
const afterBufferOptions = [
|
|
||||||
{
|
|
||||||
label: t("event_buffer_default"),
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
|
||||||
label: minutes + " " + t("minutes"),
|
|
||||||
value: minutes,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
isSearchable={false}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (val) onChange(val.value);
|
|
||||||
}}
|
|
||||||
defaultValue={
|
|
||||||
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
|
|
||||||
}
|
|
||||||
options={afterBufferOptions}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col space-y-4 pt-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
|
||||||
<div className="flex w-full items-end justify-end">
|
|
||||||
<Controller
|
|
||||||
name="minimumBookingNotice"
|
|
||||||
control={formMethods.control}
|
|
||||||
render={() => {
|
|
||||||
const durationTypeOptions = [
|
|
||||||
{
|
|
||||||
label: t("minutes"),
|
|
||||||
value: "minutes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("hours"),
|
|
||||||
value: "hours",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t("days"),
|
|
||||||
value: "days",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="w-1/2 md:w-3/4">
|
|
||||||
<InputField
|
|
||||||
required
|
|
||||||
label={t("minimum_booking_notice")}
|
|
||||||
type="number"
|
|
||||||
placeholder="120"
|
|
||||||
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
|
||||||
{...formMethods.register("minimumBookingNoticeInDurationType", {
|
|
||||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onMinimumNoticeChange(event.target.value),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
<input type="hidden" {...formMethods.register("minimumBookingNotice")} />
|
|
||||||
</div>
|
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
onChange={(val) => {
|
||||||
defaultValue={durationTypeOptions.find(
|
if (val) onChange(val.value);
|
||||||
(option) => option.value === minimumBookingNoticeType.current
|
}}
|
||||||
)}
|
defaultValue={
|
||||||
onChange={(input) => onMinimumNoticeDurationTypeChange(input?.value as DurationType)}
|
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
|
||||||
options={durationTypeOptions}
|
}
|
||||||
|
options={beforeBufferOptions}
|
||||||
/>
|
/>
|
||||||
</>
|
);
|
||||||
);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
|
||||||
|
<Controller
|
||||||
|
name="afterBufferTime"
|
||||||
|
control={formMethods.control}
|
||||||
|
defaultValue={eventType.afterEventBuffer || 0}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
const afterBufferOptions = [
|
||||||
|
{
|
||||||
|
label: t("event_buffer_default"),
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||||
|
label: minutes + " " + t("minutes"),
|
||||||
|
value: minutes,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
isSearchable={false}
|
||||||
|
onChange={(val) => {
|
||||||
|
if (val) onChange(val.value);
|
||||||
|
}}
|
||||||
|
defaultValue={
|
||||||
|
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
|
||||||
|
}
|
||||||
|
options={afterBufferOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||||
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
|
<div className="w-full">
|
||||||
<Controller
|
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
|
||||||
name="slotInterval"
|
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
|
||||||
control={formMethods.control}
|
</div>
|
||||||
render={() => {
|
<div className="w-full">
|
||||||
const slotIntervalOptions = [
|
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
|
||||||
{
|
<Controller
|
||||||
label: t("slot_interval_default"),
|
name="slotInterval"
|
||||||
value: -1,
|
control={formMethods.control}
|
||||||
},
|
render={() => {
|
||||||
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
|
const slotIntervalOptions = [
|
||||||
label: minutes + " " + t("minutes"),
|
{
|
||||||
value: minutes,
|
label: t("slot_interval_default"),
|
||||||
})),
|
value: -1,
|
||||||
];
|
},
|
||||||
return (
|
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
|
||||||
<Select
|
label: minutes + " " + t("minutes"),
|
||||||
isSearchable={false}
|
value: minutes,
|
||||||
onChange={(val) => {
|
})),
|
||||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
];
|
||||||
}}
|
return (
|
||||||
defaultValue={
|
<Select
|
||||||
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
|
isSearchable={false}
|
||||||
slotIntervalOptions[0]
|
onChange={(val) => {
|
||||||
}
|
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||||
options={slotIntervalOptions}
|
}}
|
||||||
/>
|
defaultValue={
|
||||||
);
|
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
|
||||||
}}
|
slotIntervalOptions[0]
|
||||||
/>
|
}
|
||||||
|
options={slotIntervalOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<hr />
|
||||||
<hr className="my-8" />
|
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="bookingLimits"
|
name="bookingLimits"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -276,8 +261,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<hr />
|
||||||
<hr className="my-8" />
|
|
||||||
<Controller
|
<Controller
|
||||||
name="periodType"
|
name="periodType"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -446,7 +430,7 @@ const BookingLimits = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
variant="icon"
|
||||||
StartIcon={Icon.FiTrash}
|
StartIcon={Icon.FiTrash}
|
||||||
color="destructive"
|
color="destructive"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import { MultiValue } from "react-select";
|
import { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui";
|
import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
import { slugify } from "@lib/slugify";
|
import { slugify } from "@lib/slugify";
|
||||||
|
|
||||||
@@ -160,7 +162,7 @@ export const EventSetupTab = (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 py-1.5 px-2">
|
<li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||||
<div className="flex max-w-full justify-between">
|
<div className="flex max-w-full justify-between">
|
||||||
<div key={index} className="flex flex-grow items-center">
|
<div key={index} className="flex flex-grow items-center">
|
||||||
<img
|
<img
|
||||||
@@ -195,6 +197,23 @@ export const EventSetupTab = (
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{validLocations.some((location) => location.type === MeetLocationType) && (
|
||||||
|
<div className="flex text-sm text-gray-600">
|
||||||
|
<Icon.FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
||||||
|
<Trans i18nKey="event_type_requres_google_cal">
|
||||||
|
<p>
|
||||||
|
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
||||||
|
Change it{" "}
|
||||||
|
<Link
|
||||||
|
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
|
||||||
|
className="underline">
|
||||||
|
here.
|
||||||
|
</Link>{" "}
|
||||||
|
We will fall back to Cal video if you do not change it.
|
||||||
|
</p>
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
||||||
<li>
|
<li>
|
||||||
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
||||||
@@ -326,6 +345,7 @@ export const EventSetupTab = (
|
|||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("location")}
|
{t("location")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="locations"
|
name="locations"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { SchedulingType } from "@prisma/client/";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo } from "react";
|
import { ComponentProps } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext, useWatch, Control } from "react-hook-form";
|
||||||
|
import { Options } from "react-select";
|
||||||
|
|
||||||
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
import CheckedTeamSelect, {
|
||||||
|
CheckedSelectOption,
|
||||||
|
} from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Label, Select } from "@calcom/ui";
|
import { Label, Select } from "@calcom/ui";
|
||||||
@@ -22,14 +25,66 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
|||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EventTeamTab = ({
|
const FixedHosts = ({
|
||||||
eventType,
|
control,
|
||||||
team,
|
labelText,
|
||||||
teamMembers,
|
placeholder,
|
||||||
}: Pick<EventTypeSetupProps, "eventType" | "teamMembers" | "team">) => {
|
options = [],
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
control: Control<FormValues>;
|
||||||
|
labelText: string;
|
||||||
|
placeholder: string;
|
||||||
|
options?: Options<CheckedSelectOption>;
|
||||||
|
} & Partial<ComponentProps<typeof CheckedTeamSelect>>) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
||||||
|
<div>
|
||||||
|
<Label>{labelText}</Label>
|
||||||
|
<Controller
|
||||||
|
name="hostsFixed"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => {
|
||||||
|
return (
|
||||||
|
<CheckedTeamSelect
|
||||||
|
isDisabled={false}
|
||||||
|
onChange={(options) => {
|
||||||
|
onChange(
|
||||||
|
options.map((option) => ({
|
||||||
|
isFixed: true,
|
||||||
|
userId: parseInt(option.value, 10),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
value={value
|
||||||
|
.map(
|
||||||
|
(host) =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
options.find((member) => member.value === host.userId.toString())!
|
||||||
|
)
|
||||||
|
.filter(Boolean)}
|
||||||
|
controlShouldRenderValue={false}
|
||||||
|
options={options}
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const schedulingType = useWatch({
|
||||||
|
control: formMethods.control,
|
||||||
|
name: "schedulingType",
|
||||||
|
});
|
||||||
|
|
||||||
const schedulingTypeOptions: {
|
const schedulingTypeOptions: {
|
||||||
value: SchedulingType;
|
value: SchedulingType;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -47,14 +102,13 @@ export const EventTeamTab = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const teamMembersToValues = useMemo(() => {
|
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||||
return teamMembers.map(mapUserToValue);
|
|
||||||
}, [teamMembers]);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && (
|
{team && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-5">
|
||||||
<div className="flex flex-col pb-8">
|
<div className="flex flex-col">
|
||||||
<Label>{t("scheduling_type")}</Label>
|
<Label>{t("scheduling_type")}</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="schedulingType"
|
name="schedulingType"
|
||||||
@@ -72,30 +126,75 @@ export const EventTeamTab = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
{schedulingType === SchedulingType.COLLECTIVE && (
|
||||||
<Label>{t("team")}</Label>
|
<FixedHosts
|
||||||
<Controller
|
options={teamMembersOptions}
|
||||||
name="users"
|
placeholder={t("add_attendees")}
|
||||||
|
labelText={t("team")}
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
defaultValue={eventType.users.map((user) => user.id.toString())}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<CheckedTeamSelect
|
|
||||||
isDisabled={false}
|
|
||||||
onChange={(options) => onChange(options.map((user) => user.value))}
|
|
||||||
value={value
|
|
||||||
.map(
|
|
||||||
(userId) =>
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
teamMembers.map(mapUserToValue).find((member) => member.value === userId)!
|
|
||||||
)
|
|
||||||
.filter(Boolean)}
|
|
||||||
controlShouldRenderValue={false}
|
|
||||||
options={teamMembersToValues}
|
|
||||||
placeholder={t("add_attendees")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
||||||
|
<>
|
||||||
|
<FixedHosts
|
||||||
|
options={teamMembersOptions}
|
||||||
|
isOptionDisabled={(option) =>
|
||||||
|
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
||||||
|
}
|
||||||
|
placeholder={t("add_fixed_hosts")}
|
||||||
|
labelText={t("fixed_hosts")}
|
||||||
|
control={formMethods.control}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
|
||||||
|
<div>
|
||||||
|
<Label>{t("round_robin_hosts")}</Label>
|
||||||
|
<Controller
|
||||||
|
name="hosts"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<CheckedTeamSelect
|
||||||
|
isDisabled={false}
|
||||||
|
onChange={(options) =>
|
||||||
|
onChange(
|
||||||
|
options.map((option) => ({
|
||||||
|
isFixed: false,
|
||||||
|
userId: parseInt(option.value, 10),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={value
|
||||||
|
.map(
|
||||||
|
(host) =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
teamMembers
|
||||||
|
.map(mapUserToValue)
|
||||||
|
.find((member) => member.value === host.userId.toString())!
|
||||||
|
)
|
||||||
|
.filter(Boolean)}
|
||||||
|
controlShouldRenderValue={false}
|
||||||
|
options={teamMembers.map(mapUserToValue)}
|
||||||
|
isOptionDisabled={(option) =>
|
||||||
|
!!formMethods
|
||||||
|
.getValues("hostsFixed")
|
||||||
|
.find((host) => host.userId.toString() === option.value)
|
||||||
|
}
|
||||||
|
placeholder={t("add_attendees")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/*<TextField
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
label={t("minimum_round_robin_hosts_count")}
|
||||||
|
defaultValue={1}
|
||||||
|
{...formMethods.register("minimumHostCount")}
|
||||||
|
addOnSuffix={<>{t("hosts")}</>}
|
||||||
|
/>*/}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { TFunction } from "next-i18next";
|
import { TFunction } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState, Suspense } from "react";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -15,15 +16,15 @@ import {
|
|||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
ConfirmationDialogContent,
|
ConfirmationDialogContent,
|
||||||
Dialog,
|
Dialog,
|
||||||
Divider,
|
DropdownMenuSeparator,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
HorizontalTabs,
|
HorizontalTabs,
|
||||||
Icon,
|
Icon,
|
||||||
Label,
|
Label,
|
||||||
Shell,
|
|
||||||
showToast,
|
showToast,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Switch,
|
Switch,
|
||||||
@@ -32,7 +33,6 @@ import {
|
|||||||
VerticalTabs,
|
VerticalTabs,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { ClientSuspense } from "@components/ClientSuspense";
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -56,12 +56,15 @@ function getNavigation(props: {
|
|||||||
installedAppsNumber: number;
|
installedAppsNumber: number;
|
||||||
}) {
|
}) {
|
||||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
||||||
|
const duration =
|
||||||
|
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: "event_setup_tab_title",
|
name: "event_setup_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=setup`,
|
href: `/event-types/${eventType.id}?tabName=setup`,
|
||||||
icon: Icon.FiLink,
|
icon: Icon.FiLink,
|
||||||
info: `${eventType.length} Mins`, // TODO: Get this from props
|
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "availability",
|
name: "availability",
|
||||||
@@ -152,7 +155,7 @@ function EventTypeSingleLayout({
|
|||||||
// If there is a team put this navigation item within the tabs
|
// If there is a team put this navigation item within the tabs
|
||||||
if (team) {
|
if (team) {
|
||||||
navigation.splice(2, 0, {
|
navigation.splice(2, 0, {
|
||||||
name: "scheduling_type",
|
name: "assignment",
|
||||||
href: `/event-types/${eventType.id}?tabName=team`,
|
href: `/event-types/${eventType.id}?tabName=team`,
|
||||||
icon: Icon.FiUsers,
|
icon: Icon.FiUsers,
|
||||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
||||||
@@ -205,7 +208,7 @@ function EventTypeSingleLayout({
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
size="icon"
|
variant="icon"
|
||||||
href={permalink}
|
href={permalink}
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
StartIcon={Icon.FiExternalLink}
|
StartIcon={Icon.FiExternalLink}
|
||||||
@@ -214,7 +217,7 @@ function EventTypeSingleLayout({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="icon"
|
variant="icon"
|
||||||
StartIcon={Icon.FiLink}
|
StartIcon={Icon.FiLink}
|
||||||
tooltip={t("copy_link")}
|
tooltip={t("copy_link")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -226,12 +229,12 @@ function EventTypeSingleLayout({
|
|||||||
embedUrl={encodeURIComponent(embedLink)}
|
embedUrl={encodeURIComponent(embedLink)}
|
||||||
StartIcon={Icon.FiCode}
|
StartIcon={Icon.FiCode}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="icon"
|
variant="icon"
|
||||||
tooltip={t("embed")}
|
tooltip={t("embed")}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="icon"
|
variant="icon"
|
||||||
StartIcon={Icon.FiTrash}
|
StartIcon={Icon.FiTrash}
|
||||||
tooltip={t("delete")}
|
tooltip={t("delete")}
|
||||||
disabled={!hasPermsToDelete}
|
disabled={!hasPermsToDelete}
|
||||||
@@ -247,32 +250,36 @@ function EventTypeSingleLayout({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-gray-100">
|
||||||
<Button
|
<DropdownItem
|
||||||
color="minimal"
|
|
||||||
StartIcon={Icon.FiExternalLink}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
type="button"
|
||||||
|
StartIcon={Icon.FiExternalLink}
|
||||||
href={permalink}
|
href={permalink}
|
||||||
rel="noreferrer"
|
rel="noreferrer">
|
||||||
className="min-w-full">
|
|
||||||
{t("preview")}
|
{t("preview")}
|
||||||
</Button>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-gray-100">
|
||||||
<Button color="minimal" StartIcon={Icon.FiLink}>
|
<DropdownItem
|
||||||
|
type="button"
|
||||||
|
StartIcon={Icon.FiLink}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(permalink);
|
||||||
|
showToast("Link copied!", "success");
|
||||||
|
}}>
|
||||||
{t("copy_link")}
|
{t("copy_link")}
|
||||||
</Button>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="focus:ring-gray-100">
|
<DropdownMenuItem className="focus:ring-gray-100">
|
||||||
<Button
|
<DropdownItem
|
||||||
color="minimal"
|
type="button"
|
||||||
StartIcon={Icon.FiTrash}
|
StartIcon={Icon.FiTrash}
|
||||||
disabled={!hasPermsToDelete}
|
disabled={!hasPermsToDelete}
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
onClick={() => setDeleteDialogOpen(true)}>
|
||||||
className="min-w-full">
|
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</Button>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<Divider />
|
<DropdownMenuSeparator className="block sm:hidden" />
|
||||||
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
|
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
@@ -301,7 +308,7 @@ function EventTypeSingleLayout({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<ClientSuspense fallback={<Icon.FiLoader />}>
|
<Suspense fallback={<Icon.FiLoader />}>
|
||||||
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block">
|
||||||
<VerticalTabs
|
<VerticalTabs
|
||||||
@@ -317,14 +324,14 @@ function EventTypeSingleLayout({
|
|||||||
<div className="w-full ltr:mr-2 rtl:ml-2">
|
<div className="w-full ltr:mr-2 rtl:ml-2">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 rounded-md border-neutral-200 bg-white sm:mx-0 xl:mt-0",
|
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
||||||
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
|
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ClientSuspense>
|
</Suspense>
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isLoading={deleteMutation.isLoading}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function RecurringEventController({
|
|||||||
{recurringEventState && (
|
{recurringEventState && (
|
||||||
<div data-testid="recurring-event-collapsible" className="text-sm">
|
<div data-testid="recurring-event-collapsible" className="text-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -89,7 +89,7 @@ export default function RecurringEventController({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex items-center">
|
<div className="mt-4 flex items-center">
|
||||||
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -105,7 +105,7 @@ export default function RecurringEventController({
|
|||||||
setRecurringEventState(newVal);
|
setRecurringEventState(newVal);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">
|
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">
|
||||||
{t("events", {
|
{t("events", {
|
||||||
count: recurringEventState.count,
|
count: recurringEventState.count,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function SkeletonLoader() {
|
|||||||
<SkeletonText className="h-4 w-24" />
|
<SkeletonText className="h-4 w-24" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
<SkeletonItem />
|
<SkeletonItem />
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps)
|
|||||||
<>
|
<>
|
||||||
<div className="mt-6 flex flex-row">
|
<div className="mt-6 flex flex-row">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-neutral-700">
|
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-gray-700">
|
||||||
{t("create_events_on")}
|
{t("create_events_on")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
name="avatar"
|
name="avatar"
|
||||||
id="avatar"
|
id="avatar"
|
||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-neutral-800 focus:outline-none focus:ring-neutral-800"
|
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-gray-800 focus:outline-none focus:ring-gray-800"
|
||||||
defaultValue={imageSrc}
|
defaultValue={imageSrc}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center px-4">
|
<div className="flex items-center px-4">
|
||||||
@@ -146,7 +146,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
ref={bioRef}
|
ref={bioRef}
|
||||||
name="bio"
|
name="bio"
|
||||||
id="bio"
|
id="bio"
|
||||||
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
|
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm"
|
||||||
defaultValue={user?.bio || undefined}
|
defaultValue={user?.bio || undefined}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setValue("bio", event.target.value);
|
setValue("bio", event.target.value);
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ interface IUserSettingsProps {
|
|||||||
const UserSettings = (props: IUserSettingsProps) => {
|
const UserSettings = (props: IUserSettingsProps) => {
|
||||||
const { user, nextStep } = props;
|
const { user, nextStep } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState(user.timeZone ?? dayjs.tz.guess());
|
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
||||||
/>
|
/>
|
||||||
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
||||||
<p className="font-medium text-neutral-900 dark:text-white">{member.name}</p>
|
<p className="font-medium text-gray-900 dark:text-white">{member.name}</p>
|
||||||
<p className="line-clamp-3 overflow-ellipsis text-sm font-normal text-neutral-500 dark:text-white">
|
<p className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white">
|
||||||
{member.bio || t("user_from_team", { user: member.name, team: teamName })}
|
{member.bio || t("user_from_team", { user: member.name, team: teamName })}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
||||||
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
|
{props.heading && <h2 className="font-cal text-center text-3xl text-gray-900">{props.heading}</h2>}
|
||||||
</div>
|
</div>
|
||||||
{props.loading && (
|
{props.loading && (
|
||||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
|
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
|
||||||
@@ -32,10 +32,10 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
|
<div className="mx-2 rounded-md border border-gray-200 bg-white px-4 py-10 sm:px-10">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 text-center text-sm text-neutral-600">
|
<div className="mt-8 text-center text-sm text-gray-600">
|
||||||
{props.footerText}
|
{props.footerText}
|
||||||
<Credits />
|
<Credits />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,13 +35,7 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2">
|
<li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2">
|
||||||
{item.href ? (
|
{item.href ? <Link href={item.href}>{avatar}</Link> : avatar}
|
||||||
<Link href={item.href}>
|
|
||||||
<a>{avatar}</a>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
avatar
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
{...props}
|
{...props}
|
||||||
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
||||||
<props.Icon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
<props.Icon className="h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
|
||||||
{props.children}
|
{props.children}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
interface ILinkTextProps {
|
|
||||||
href: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
classNameChildren?: string;
|
|
||||||
target?: string;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This component had to be made in order to make i18n work with next/link
|
|
||||||
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
|
|
||||||
**/
|
|
||||||
export const LinkText = (props: ILinkTextProps) => {
|
|
||||||
const { target, href, children, classNameChildren, ...moreProps } = props;
|
|
||||||
return (
|
|
||||||
<Link href={href || ""} {...moreProps}>
|
|
||||||
<a target={target || ""} className={classNameChildren}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -9,26 +9,21 @@ const PoweredByCal = () => {
|
|||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
return (
|
return (
|
||||||
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
|
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
|
||||||
<Link href={POWERED_BY_URL}>
|
<Link
|
||||||
<a target="_blank" className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
|
href={POWERED_BY_URL}
|
||||||
{t("powered_by")}{" "}
|
target="_blank"
|
||||||
{
|
className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
{t("powered_by")}{" "}
|
||||||
<img
|
<img
|
||||||
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
||||||
src="/cal-logo-word.svg"
|
src="/cal-logo-word.svg"
|
||||||
alt="Cal.com Logo"
|
alt="Cal.com Logo"
|
||||||
/>
|
/>
|
||||||
}
|
<img
|
||||||
{
|
className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
src="/cal-logo-word-dark.svg"
|
||||||
<img
|
alt="Cal.com Logo"
|
||||||
className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
|
/>
|
||||||
src="/cal-logo-word-dark.svg"
|
|
||||||
alt="Cal.com Logo"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export default function SettingInputContainer({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="block sm:flex">
|
<div className="block sm:flex">
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-neutral-700">
|
<label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-gray-700">
|
||||||
<Icon className="mt-0.5 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
<Icon className="mt-0.5 h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -187,10 +187,10 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<div className="flex justify-items-center">
|
<div className="flex justify-items-center">
|
||||||
<Label htmlFor="username">{t("username")}</Label>
|
<Label htmlFor="username">{t("username")}</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex rounded-md">
|
<div className="flex rounded-md">
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isInputUsernamePremium ? "border-1 border-orange-400 " : "",
|
isInputUsernamePremium ? "border border-orange-400 " : "",
|
||||||
"hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex"
|
"hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex"
|
||||||
)}>
|
)}>
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
|
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
|
||||||
@@ -207,8 +207,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
"border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
|
"border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
|
||||||
isInputUsernamePremium
|
isInputUsernamePremium
|
||||||
? "border-1 focus:border-1 border-orange-400 focus:border-orange-400"
|
? "border border-orange-400 focus:border focus:border-orange-400"
|
||||||
: "border-1 focus:border-2",
|
: "border focus:border",
|
||||||
markAsError
|
markAsError
|
||||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
|
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
|
||||||
: "border-l-gray-300",
|
: "border-l-gray-300",
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-2 flex rounded-md">
|
<div className="flex rounded-md">
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<TextField
|
<TextField
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
|||||||
{React.createElement(
|
{React.createElement(
|
||||||
descriptionAsLabel ? "div" : "label",
|
descriptionAsLabel ? "div" : "label",
|
||||||
{
|
{
|
||||||
className: "flex text-sm font-medium text-neutral-700",
|
className: "flex text-sm font-medium text-gray-700",
|
||||||
...(!descriptionAsLabel
|
...(!descriptionAsLabel
|
||||||
? {
|
? {
|
||||||
htmlFor: rest.id,
|
htmlFor: rest.id,
|
||||||
@@ -39,7 +39,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
|||||||
{
|
{
|
||||||
className: classNames(
|
className: classNames(
|
||||||
"relative flex items-start",
|
"relative flex items-start",
|
||||||
descriptionAsLabel ? "text-neutral-700" : "text-neutral-900"
|
descriptionAsLabel ? "text-gray-700" : "text-gray-900"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const CheckedSelect = ({
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{value.map((option) => (
|
{value.map((option) => (
|
||||||
<div key={option.value} className="border-1 border p-2 font-medium">
|
<div key={option.value} className="border p-2 font-medium">
|
||||||
<Avatar
|
<Avatar
|
||||||
className="inline h-6 w-6 rounded-full ltr:mr-2 rtl:ml-2"
|
className="inline h-6 w-6 rounded-full ltr:mr-2 rtl:ml-2"
|
||||||
imageSrc={option.avatar}
|
imageSrc={option.avatar}
|
||||||
@@ -50,7 +50,7 @@ export const CheckedSelect = ({
|
|||||||
{option.label}
|
{option.label}
|
||||||
<Icon.FiX
|
<Icon.FiX
|
||||||
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
||||||
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-neutral-500"
|
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
|
|||||||
<div className="block sm:flex">
|
<div className="block sm:flex">
|
||||||
{!!label && (
|
{!!label && (
|
||||||
<div className="min-w-48 mb-4 sm:mb-0">
|
<div className="min-w-48 mb-4 sm:mb-0">
|
||||||
<label htmlFor={rest.id} className="flex h-full items-center text-sm font-medium text-neutral-700">
|
<label htmlFor={rest.id} className="flex h-full items-center text-sm font-medium text-gray-700">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import React, { ReactNode, useState } from "react";
|
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
|
||||||
|
|
||||||
type RadioAreaProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
defaultChecked?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RadioArea = (props: RadioAreaProps) => {
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
className={classNames(
|
|
||||||
"border-1 block border p-4 focus:outline-none focus:ring focus:ring-neutral-500",
|
|
||||||
props.checked && "border-brand",
|
|
||||||
props.className
|
|
||||||
)}>
|
|
||||||
<input
|
|
||||||
onChange={(e) => {
|
|
||||||
if (typeof props.onChange === "function") {
|
|
||||||
props.onChange(e.target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
checked={props.checked}
|
|
||||||
className="float-right ml-3 text-neutral-900 focus:ring-neutral-500"
|
|
||||||
name={props.name}
|
|
||||||
value={props.value}
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
{props.children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChildrenProps = {
|
|
||||||
props: RadioAreaProps;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
|
||||||
children: ChildrenProps | ChildrenProps[];
|
|
||||||
name?: string;
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
|
|
||||||
const [checkedIdx, setCheckedIdx] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const changeHandler = (value: string, idx: number) => {
|
|
||||||
if (onChange) {
|
|
||||||
onChange(value);
|
|
||||||
}
|
|
||||||
setCheckedIdx(idx);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...passThroughProps}>
|
|
||||||
{(Array.isArray(children) ? children : [children]).map((child, idx: number) => {
|
|
||||||
if (checkedIdx === null && child.props.defaultChecked) {
|
|
||||||
setCheckedIdx(idx);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
{...child.props}
|
|
||||||
key={idx}
|
|
||||||
name={name}
|
|
||||||
checked={idx === checkedIdx}
|
|
||||||
onChange={(value: string) => changeHandler(value, idx)}>
|
|
||||||
{child.props.children}
|
|
||||||
</Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Item = RadioArea;
|
|
||||||
const Group = RadioAreaGroup;
|
|
||||||
|
|
||||||
export { RadioArea, RadioAreaGroup, Item, Group };
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Icon } from "@calcom/ui";
|
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import { RadioArea, RadioAreaGroup } from "@components/ui/form/radio-area/RadioAreaGroup";
|
|
||||||
|
|
||||||
interface OptionProps
|
|
||||||
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RadioAreaSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
|
|
||||||
options: OptionProps[]; // allow options to be passed programmatically, like options={}
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const {
|
|
||||||
options,
|
|
||||||
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
|
|
||||||
placeholder = t("select"),
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
|
|
||||||
options.find((option: OptionProps) => option.value === value)?.label;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapsible className={classNames("w-full", props.className)}>
|
|
||||||
<CollapsibleTrigger
|
|
||||||
type="button"
|
|
||||||
disabled={disabled}
|
|
||||||
className={classNames(
|
|
||||||
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left text-sm",
|
|
||||||
disabled && "cursor-default bg-gray-200 focus:ring-0 "
|
|
||||||
)}>
|
|
||||||
{getLabel(props.value) ?? placeholder}
|
|
||||||
<Icon.FiChevronDown className="float-right h-5 w-5 text-neutral-500" />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<RadioAreaGroup className="space-y-2 text-sm" name={props.name} onChange={props.onChange}>
|
|
||||||
{options.map((option) => (
|
|
||||||
<RadioArea
|
|
||||||
{...option}
|
|
||||||
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}
|
|
||||||
defaultChecked={props.value === option.value}>
|
|
||||||
<strong className="mb-1 block">{option.label}</strong>
|
|
||||||
<p>{option.description}</p>
|
|
||||||
</RadioArea>
|
|
||||||
))}
|
|
||||||
</RadioAreaGroup>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Select;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./RadioAreaGroup";
|
|
||||||
export * from "./Select";
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// lets refactor and move this into packages/lib/hooks/
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
const useMediaQuery = (query: string) => {
|
const useMediaQuery = (query: string) => {
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ const nextConfig = {
|
|||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: !!process.env.CI,
|
ignoreDuringBuilds: !!process.env.CI,
|
||||||
},
|
},
|
||||||
|
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
||||||
|
// modularizeImports: {
|
||||||
|
// "@calcom/ui": {
|
||||||
|
// transform: "@calcom/ui/components/{{member}}",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.4.4",
|
"version": "2.5.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"dotenv-cli": "^6.0.0",
|
"dotenv-cli": "^6.0.0",
|
||||||
|
"eslint-config-next": "^13.1.1",
|
||||||
"googleapis": "^84.0.0",
|
"googleapis": "^84.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
@@ -80,16 +81,14 @@
|
|||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"micro": "^9.4.1",
|
"micro": "^9.4.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"next": "^12.3.1",
|
"next": "^13.1.1",
|
||||||
"next-auth": "^4.10.3",
|
"next-auth": "^4.10.3",
|
||||||
"next-axiom": "^0.10.0",
|
"next-axiom": "^0.16.0",
|
||||||
"next-collect": "^0.2.1",
|
"next-collect": "^0.2.1",
|
||||||
"next-i18next": "^11.3.0",
|
"next-i18next": "^11.3.0",
|
||||||
"next-mdx-remote": "^4.1.0",
|
|
||||||
"next-seo": "^4.26.0",
|
"next-seo": "^4.26.0",
|
||||||
"next-themes": "^0.2.0",
|
"next-themes": "^0.2.0",
|
||||||
"next-transpile-modules": "^9.0.0",
|
"next-transpile-modules": "^10.0.0",
|
||||||
"nock": "^13.2.8",
|
|
||||||
"nodemailer": "^6.7.8",
|
"nodemailer": "^6.7.8",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
|
|||||||
+46
-48
@@ -98,26 +98,26 @@ export default function Custom404() {
|
|||||||
|
|
||||||
<ul role="list" className="divide-y divide-gray-200 border-gray-200">
|
<ul role="list" className="divide-y divide-gray-200 border-gray-200">
|
||||||
<li className="px-4 py-2">
|
<li className="px-4 py-2">
|
||||||
<Link href={`${DEVELOPER_DOCS}/self-hosting/installation`}>
|
<Link
|
||||||
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
href={`${DEVELOPER_DOCS}/self-hosting/installation`}
|
||||||
<div className="flex-shrink-0">
|
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
<div className="flex-shrink-0">
|
||||||
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
||||||
|
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="text-base font-medium text-gray-900">
|
||||||
|
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
||||||
|
<span className="absolute inset-0" aria-hidden="true" />
|
||||||
|
{t("prisma_studio_tip")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</h3>
|
||||||
<div className="min-w-0 flex-1">
|
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
|
||||||
<h3 className="text-base font-medium text-gray-900">
|
</div>
|
||||||
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
<div className="flex-shrink-0 self-center">
|
||||||
<span className="absolute inset-0" aria-hidden="true" />
|
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
{t("prisma_studio_tip")}
|
</div>
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-shrink-0 self-center">
|
|
||||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="px-4 py-2">
|
<li className="px-4 py-2">
|
||||||
@@ -167,11 +167,11 @@ export default function Custom404() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Link href={`${WEBSITE_URL}/enterprise`}>
|
<Link
|
||||||
<a className="text-base font-medium text-black hover:text-gray-500">
|
href={`${WEBSITE_URL}/enterprise`}
|
||||||
{t("contact_sales")}
|
className="text-base font-medium text-black hover:text-gray-500">
|
||||||
<span aria-hidden="true"> →</span>
|
{t("contact_sales")}
|
||||||
</a>
|
<span aria-hidden="true"> →</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,26 +243,26 @@ export default function Custom404() {
|
|||||||
<ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200">
|
<ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200">
|
||||||
{links.map((link, linkIdx) => (
|
{links.map((link, linkIdx) => (
|
||||||
<li key={linkIdx} className="px-4 py-2">
|
<li key={linkIdx} className="px-4 py-2">
|
||||||
<Link href={link.href}>
|
<Link
|
||||||
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
href={link.href}
|
||||||
<div className="flex-shrink-0">
|
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
<div className="flex-shrink-0">
|
||||||
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
||||||
|
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="text-base font-medium text-gray-900">
|
||||||
|
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
||||||
|
<span className="absolute inset-0" aria-hidden="true" />
|
||||||
|
{link.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</h3>
|
||||||
<div className="min-w-0 flex-1">
|
<p className="text-base text-gray-500">{link.description}</p>
|
||||||
<h3 className="text-base font-medium text-gray-900">
|
</div>
|
||||||
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
<div className="flex-shrink-0 self-center">
|
||||||
<span className="absolute inset-0" aria-hidden="true" />
|
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
{link.title}
|
</div>
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
<p className="text-base text-gray-500">{link.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-shrink-0 self-center">
|
|
||||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -313,11 +313,9 @@ export default function Custom404() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Link href="/">
|
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
|
||||||
<a className="text-base font-medium text-black hover:text-gray-500">
|
{t("or_go_back_home")}
|
||||||
{t("or_go_back_home")}
|
<span aria-hidden="true"> →</span>
|
||||||
<span aria-hidden="true"> →</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+39
-44
@@ -1,6 +1,5 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -27,12 +26,11 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Icon, HeadSeo } from "@calcom/ui";
|
import { Icon, HeadSeo, AvatarGroup } from "@calcom/ui";
|
||||||
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import { EmbedProps } from "@lib/withEmbedSsr";
|
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
|
||||||
import { AvatarSSR } from "@components/ui/AvatarSSR";
|
import { AvatarSSR } from "@components/ui/AvatarSSR";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
@@ -58,27 +56,27 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
{eventTypes.map((type, index) => (
|
{eventTypes.map((type, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600">
|
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
|
||||||
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||||
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}>
|
<Link
|
||||||
<a className="flex justify-between px-6 py-4" data-testid="event-type-link">
|
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
|
||||||
<div className="flex-shrink">
|
className="flex justify-between px-6 py-4"
|
||||||
<p className="dark:text-darkgray-700 text-sm font-semibold text-neutral-900">{type.title}</p>
|
data-testid="event-type-link">
|
||||||
<EventTypeDescription className="text-sm" eventType={type} />
|
<div className="flex-shrink">
|
||||||
</div>
|
<p className="dark:text-darkgray-700 text-sm font-semibold text-gray-900">{type.title}</p>
|
||||||
<div className="mt-1 self-center">
|
<EventTypeDescription className="text-sm" eventType={type} />
|
||||||
<AvatarGroup
|
</div>
|
||||||
border="border-2 border-white dark:border-darkgray-100"
|
<div className="mt-1 self-center">
|
||||||
truncateAfter={4}
|
<AvatarGroup
|
||||||
className="flex flex-shrink-0"
|
truncateAfter={4}
|
||||||
size={10}
|
className="flex flex-shrink-0"
|
||||||
items={props.users.map((user) => ({
|
size="sm"
|
||||||
alt: user.name || "",
|
items={props.users.map((user) => ({
|
||||||
image: user.avatar || "",
|
alt: user.name || "",
|
||||||
}))}
|
image: user.avatar || "",
|
||||||
/>
|
}))}
|
||||||
</div>
|
/>
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -135,20 +133,19 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
{isSingleUser && ( // When we deal with a single user, not dynamic group
|
{isSingleUser && ( // When we deal with a single user, not dynamic group
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername} />
|
<AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername} />
|
||||||
<h1 className="font-cal mb-1 text-3xl text-neutral-900 dark:text-white">
|
<h1 className="font-cal mb-1 text-3xl text-gray-900 dark:text-white">
|
||||||
{nameOrUsername}
|
{nameOrUsername}
|
||||||
{user.verified && (
|
{user.verified && (
|
||||||
<Icon.BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
|
<Icon.BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="dark:text-darkgray-600 text-s text-neutral-500">{user.bio}</p>
|
<p className="dark:text-darkgray-600 text-s text-gray-500">{user.bio}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"rounded-md ",
|
"rounded-md ",
|
||||||
!isEventListEmpty &&
|
!isEventListEmpty && "border border-gray-200 dark:border-gray-700 dark:hover:border-gray-600"
|
||||||
"border border-neutral-200 dark:border-neutral-700 dark:hover:border-neutral-600"
|
|
||||||
)}
|
)}
|
||||||
data-testid="event-types">
|
data-testid="event-types">
|
||||||
{user.away ? (
|
{user.away ? (
|
||||||
@@ -167,7 +164,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<div
|
<div
|
||||||
key={type.id}
|
key={type.id}
|
||||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||||
className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600">
|
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
|
||||||
<Icon.FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
<Icon.FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||||
<Link
|
<Link
|
||||||
@@ -175,22 +172,20 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
href={{
|
href={{
|
||||||
pathname: `/${user.username}/${type.slug}`,
|
pathname: `/${user.username}/${type.slug}`,
|
||||||
query,
|
query,
|
||||||
}}>
|
}}
|
||||||
<a
|
onClick={async () => {
|
||||||
onClick={async () => {
|
sdkActionManager?.fire("eventTypeSelected", {
|
||||||
sdkActionManager?.fire("eventTypeSelected", {
|
eventType: type,
|
||||||
eventType: type,
|
});
|
||||||
});
|
}}
|
||||||
}}
|
className="block w-full p-5"
|
||||||
className="block w-full p-5"
|
data-testid="event-type-link">
|
||||||
data-testid="event-type-link">
|
<div className="flex flex-wrap items-center">
|
||||||
<div className="flex flex-wrap items-center">
|
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
||||||
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
{type.title}
|
||||||
{type.title}
|
</h2>
|
||||||
</h2>
|
</div>
|
||||||
</div>
|
<EventTypeDescription eventType={type} />
|
||||||
<EventTypeDescription eventType={type} />
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function Type(props: AvailabilityPageProps) {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return props.away ? (
|
return props.away ? (
|
||||||
<div className="h-screen dark:bg-neutral-900">
|
<div className="h-screen dark:bg-gray-900">
|
||||||
<main className="mx-auto max-w-3xl px-4 py-24">
|
<main className="mx-auto max-w-3xl px-4 py-24">
|
||||||
<div className="space-y-6" data-testid="event-types">
|
<div className="space-y-6" data-testid="event-types">
|
||||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
|
|||||||
export default function Book(props: BookPageProps) {
|
export default function Book(props: BookPageProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return props.away ? (
|
return props.away ? (
|
||||||
<div className="h-screen dark:bg-neutral-900">
|
<div className="h-screen dark:bg-gray-900">
|
||||||
<main className="mx-auto max-w-3xl px-4 py-24">
|
<main className="mx-auto max-w-3xl px-4 py-24">
|
||||||
<div className="space-y-6" data-testid="event-types">
|
<div className="space-y-6" data-testid="event-types">
|
||||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||||
@@ -42,7 +42,7 @@ export default function Book(props: BookPageProps) {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
|
) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
|
||||||
<div className="h-screen dark:bg-neutral-900">
|
<div className="h-screen dark:bg-gray-900">
|
||||||
<main className="mx-auto max-w-3xl px-4 py-24">
|
<main className="mx-auto max-w-3xl px-4 py-24">
|
||||||
<div className="space-y-6" data-testid="event-types">
|
<div className="space-y-6" data-testid="event-types">
|
||||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ class MyDocument extends Document<Props> {
|
|||||||
return (
|
return (
|
||||||
<Html lang={locale} dir={dir}>
|
<Html lang={locale} dir={dir}>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="preload" href="/cal.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
@@ -82,7 +81,7 @@ class MyDocument extends Document<Props> {
|
|||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100"
|
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100 antialiased"
|
||||||
style={
|
style={
|
||||||
this.props.isEmbed
|
this.props.isEmbed
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { hashPassword } from "@calcom/lib/auth";
|
||||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { hashPassword } from "@lib/auth";
|
|
||||||
import slugify from "@lib/slugify";
|
import slugify from "@lib/slugify";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
@@ -12,6 +12,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
|
||||||
|
res.status(403).json({ message: "Signup is disabled" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
const { email, password } = data;
|
const { email, password } = data;
|
||||||
const username = slugify(data.username);
|
const username = slugify(data.username);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
location: true,
|
location: true,
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
email: true,
|
email: true,
|
||||||
name: true,
|
name: true,
|
||||||
username: true,
|
username: true,
|
||||||
@@ -100,6 +101,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
startTime: booking.startTime.toISOString(),
|
startTime: booking.startTime.toISOString(),
|
||||||
endTime: booking.endTime.toISOString(),
|
endTime: booking.endTime.toISOString(),
|
||||||
organizer: {
|
organizer: {
|
||||||
|
id: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name,
|
name,
|
||||||
timeZone: user.timeZone,
|
timeZone: user.timeZone,
|
||||||
|
|||||||
@@ -35,6 +35,24 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
uid: "xxyPr4cg2xx4XoS2KeMEQy",
|
uid: "xxyPr4cg2xx4XoS2KeMEQy",
|
||||||
metadata: {},
|
metadata: {},
|
||||||
recurringEvent: null,
|
recurringEvent: null,
|
||||||
|
appsStatus: [
|
||||||
|
{
|
||||||
|
appName: "Outlook Calendar",
|
||||||
|
type: "office365_calendar",
|
||||||
|
success: 1,
|
||||||
|
failures: 0,
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: "Google Meet",
|
||||||
|
type: "conferencing",
|
||||||
|
success: 0,
|
||||||
|
failures: 1,
|
||||||
|
errors: [],
|
||||||
|
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
req.statusCode = 200;
|
req.statusCode = 200;
|
||||||
@@ -42,9 +60,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("OrganizerRequestEmail", {
|
renderEmail("OrganizerScheduledEmail", {
|
||||||
attendee: evt.attendees[0],
|
|
||||||
calEvent: evt,
|
calEvent: evt,
|
||||||
|
attendee: evt.organizer,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
res.end();
|
res.end();
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
||||||
|
import { getSession } from "@calcom/lib/auth";
|
||||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
|
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
const defaultIntegrationAddHandler = async ({
|
const defaultIntegrationAddHandler = async ({
|
||||||
|
|||||||
@@ -1,34 +1,18 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import matter from "gray-matter";
|
import matter from "gray-matter";
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||||
import { MDXRemote } from "next-mdx-remote";
|
|
||||||
import { serialize } from "next-mdx-remote/serialize";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||||
|
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import App from "@components/apps/App";
|
import App from "@components/apps/App";
|
||||||
|
|
||||||
const components = {
|
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||||
a: ({ href = "", ...otherProps }: JSX.IntrinsicElements["a"]) => (
|
|
||||||
<Link href={href}>
|
|
||||||
<a {...otherProps} />
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
img: ({ src = "", alt = "", placeholder, ...rest }: JSX.IntrinsicElements["img"]) => (
|
|
||||||
<Image src={src} alt={alt} {...rest} />
|
|
||||||
),
|
|
||||||
// @TODO: In v2 the slider isn't shown anymore. However, to ensure the v1 pages keep
|
|
||||||
// working, this component is still rendered in the MDX content. To skip them in the v2
|
|
||||||
// content we have to render null here. In v2 the gallery is shown by directly
|
|
||||||
// using the `items` property from the MDX's meta data.
|
|
||||||
Slider: () => <></>,
|
|
||||||
};
|
|
||||||
|
|
||||||
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||||
return (
|
return (
|
||||||
@@ -50,10 +34,16 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
|||||||
email={data.email}
|
email={data.email}
|
||||||
licenseRequired={data.licenseRequired}
|
licenseRequired={data.licenseRequired}
|
||||||
isProOnly={data.isProOnly}
|
isProOnly={data.isProOnly}
|
||||||
images={source?.scope?.items as string[] | undefined}
|
images={source.data?.items as string[] | undefined}
|
||||||
|
isTemplate={data.isTemplate}
|
||||||
// tos="https://zoom.us/terms"
|
// tos="https://zoom.us/terms"
|
||||||
// privacy="https://zoom.us/privacy"
|
// privacy="https://zoom.us/privacy"
|
||||||
body={<MDXRemote {...source} components={components} />}
|
body={
|
||||||
|
<>
|
||||||
|
{data.slug === "google-meet" && <ExisitingGoogleCal />}
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,25 +71,34 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||||||
|
|
||||||
if (!singleApp) return { notFound: true };
|
if (!singleApp) return { notFound: true };
|
||||||
|
|
||||||
const appDirname = app.dirName;
|
const isTemplate = singleApp.isTemplate;
|
||||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/README.mdx`);
|
const appDirname = path.join(isTemplate ? "templates" : "", app.dirName);
|
||||||
|
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||||
const postFilePath = path.join(README_PATH);
|
const postFilePath = path.join(README_PATH);
|
||||||
let source = "";
|
let source = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/* If the app doesn't have a README we fallback to the package description */
|
|
||||||
source = fs.readFileSync(postFilePath).toString();
|
source = fs.readFileSync(postFilePath).toString();
|
||||||
|
source = source.replace(/{DESCRIPTION}/g, singleApp.description);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`No README.mdx provided for: ${appDirname}`);
|
/* If the app doesn't have a README we fallback to the package description */
|
||||||
|
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||||
source = singleApp.description;
|
source = singleApp.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { content, data } = matter(source);
|
const { content, data } = matter(source);
|
||||||
const mdxSource = await serialize(content, { scope: data });
|
if (data.items) {
|
||||||
|
data.items = data.items.map((item: string) => {
|
||||||
|
if (!item.includes("/api/app-store")) {
|
||||||
|
// Make relative paths absolute
|
||||||
|
return `/api/app-store/${appDirname}/${item}`;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
source: mdxSource,
|
source: { content, data },
|
||||||
data: singleApp,
|
data: singleApp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { AppCard, Shell, SkeletonText } from "@calcom/ui";
|
import { AppCard, SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
|
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
const { t, isLocaleReady } = useLocale();
|
const { t, isLocaleReady } = useLocale();
|
||||||
@@ -20,10 +21,10 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
|||||||
backPath="/apps"
|
backPath="/apps"
|
||||||
heading={
|
heading={
|
||||||
<>
|
<>
|
||||||
<Link href="/apps">
|
<Link
|
||||||
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
href="/apps"
|
||||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "}
|
className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||||
</a>
|
{isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "}
|
||||||
</Link>
|
</Link>
|
||||||
{category && (
|
{category && (
|
||||||
<span className="gap-1 text-gray-600">
|
<span className="gap-1 text-gray-600">
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { InferGetStaticPropsType } from "next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon, Shell, SkeletonText } from "@calcom/ui";
|
import { Icon, SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
|
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
const { t, isLocaleReady } = useLocale();
|
const { t, isLocaleReady } = useLocale();
|
||||||
@@ -11,28 +12,28 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
|||||||
return (
|
return (
|
||||||
<Shell isPublic large>
|
<Shell isPublic large>
|
||||||
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
|
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
|
||||||
<Link href="/apps">
|
<Link
|
||||||
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
href="/apps"
|
||||||
<Icon.FiArrowLeft className="h-4 w-4" />
|
className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
|
<Icon.FiArrowLeft className="h-4 w-4" />
|
||||||
</a>
|
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-16">
|
<div className="mb-16">
|
||||||
<div className="grid h-auto w-full grid-cols-5 gap-3">
|
<div className="grid h-auto w-full grid-cols-5 gap-3">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<Link key={category.name} href={"/apps/categories/" + category.name}>
|
<Link
|
||||||
<a
|
key={category.name}
|
||||||
data-testid={`app-store-category-${category.name}`}
|
href={"/apps/categories/" + category.name}
|
||||||
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
data-testid={`app-store-category-${category.name}`}
|
||||||
<div className="self-center">
|
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
||||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
<div className="self-center">
|
||||||
<p className="text-sm text-gray-500">
|
<h3 className="font-medium capitalize">{category.name}</h3>
|
||||||
{t("number_apps", { count: category.count })}{" "}
|
<p className="text-sm text-gray-500">
|
||||||
<Icon.FiArrowRight className="inline-block h-4 w-4" />
|
{t("number_apps", { count: category.count })}{" "}
|
||||||
</p>
|
<Icon.FiArrowRight className="inline-block h-4 w-4" />
|
||||||
</div>
|
</p>
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,12 +7,31 @@ import { getSession } from "@calcom/lib/auth";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { AppCategories } from "@calcom/prisma/client";
|
import type { AppCategories } from "@calcom/prisma/client";
|
||||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
import { AllApps, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
|
import {
|
||||||
|
AllApps,
|
||||||
|
AppStoreCategories,
|
||||||
|
HorizontalTabItemProps,
|
||||||
|
HorizontalTabs,
|
||||||
|
Icon,
|
||||||
|
TextField,
|
||||||
|
PopularAppsSlider,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
|
const tabs: HorizontalTabItemProps[] = [
|
||||||
|
{
|
||||||
|
name: "app_store",
|
||||||
|
href: "/apps",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "installed_apps",
|
||||||
|
href: "/apps/installed",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
function AppsSearch({
|
function AppsSearch({
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
@@ -43,20 +62,30 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
|
|||||||
heading={t("app_store")}
|
heading={t("app_store")}
|
||||||
subtitle={t("app_store_description")}
|
subtitle={t("app_store_description")}
|
||||||
actions={(className) => (
|
actions={(className) => (
|
||||||
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
|
<div className="flex w-full flex-col md:flex-row md:justify-between lg:w-auto">
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<HorizontalTabs tabs={tabs} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
headerClassName="sm:hidden lg:block hidden"
|
||||||
emptyStore={!appStore.length}>
|
emptyStore={!appStore.length}>
|
||||||
{!searchText && (
|
<div className="flex flex-col gap-y-8">
|
||||||
<>
|
{!searchText && (
|
||||||
<AppStoreCategories categories={categories} />
|
<>
|
||||||
<TrendingAppsSlider items={appStore} />
|
<AppStoreCategories categories={categories} />
|
||||||
</>
|
<PopularAppsSlider items={appStore} />
|
||||||
)}
|
</>
|
||||||
<AllApps
|
)}
|
||||||
apps={appStore}
|
<AllApps
|
||||||
searchText={searchText}
|
apps={appStore}
|
||||||
categories={categories.map((category) => category.name)}
|
searchText={searchText}
|
||||||
/>
|
categories={categories.map((category) => category.name)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</AppsLayout>
|
</AppsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
Icon,
|
Icon,
|
||||||
List,
|
List,
|
||||||
ShellSubHeading,
|
|
||||||
AppSkeletonLoader as SkeletonLoader,
|
AppSkeletonLoader as SkeletonLoader,
|
||||||
|
ShellSubHeading,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
@@ -92,8 +92,8 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IntegrationsContainerProps {
|
interface IntegrationsContainerProps {
|
||||||
variant?: keyof typeof InstalledAppVariants;
|
variant?: typeof InstalledAppVariants[number];
|
||||||
exclude?: (keyof typeof InstalledAppVariants)[];
|
exclude?: typeof InstalledAppVariants[number][];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IntegrationsListProps {
|
interface IntegrationsListProps {
|
||||||
@@ -115,6 +115,7 @@ const IntegrationsList = ({ data }: IntegrationsListProps) => {
|
|||||||
logo={item.logo}
|
logo={item.logo}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
separate={true}
|
separate={true}
|
||||||
|
isTemplate={item.isTemplate}
|
||||||
invalidCredential={item.invalidCredentialIds.length > 0}
|
invalidCredential={item.invalidCredentialIds.length > 0}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex w-16 justify-end">
|
<div className="flex w-16 justify-end">
|
||||||
@@ -143,8 +144,10 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
|
|||||||
automation: Icon.FiShare2,
|
automation: Icon.FiShare2,
|
||||||
analytics: Icon.FiBarChart,
|
analytics: Icon.FiBarChart,
|
||||||
payment: Icon.FiCreditCard,
|
payment: Icon.FiCreditCard,
|
||||||
|
web3: Icon.FiBarChart,
|
||||||
other: Icon.FiGrid,
|
other: Icon.FiGrid,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryCell
|
<QueryCell
|
||||||
query={query}
|
query={query}
|
||||||
@@ -183,6 +186,7 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
|
|||||||
buttonRaw={
|
buttonRaw={
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
data-testid={`connect-${variant || "other"}-apps`}
|
||||||
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
|
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
|
||||||
{t(`connect_${variant || "other"}_apps`)}
|
{t(`connect_${variant || "other"}_apps`)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -197,31 +201,29 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
|
|||||||
};
|
};
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
category: z.nativeEnum(InstalledAppVariants),
|
category: z.enum(InstalledAppVariants),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type querySchemaType = z.infer<typeof querySchema>;
|
||||||
|
|
||||||
export default function InstalledApps() {
|
export default function InstalledApps() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const category = router.query.category;
|
const category = router.query.category as querySchemaType["category"];
|
||||||
|
const categoryList: querySchemaType["category"][] = [
|
||||||
|
"payment",
|
||||||
|
"conferencing",
|
||||||
|
"automation",
|
||||||
|
"analytics",
|
||||||
|
"web3",
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
|
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
|
||||||
{(category === InstalledAppVariants.payment || category === InstalledAppVariants.conferencing) && (
|
{categoryList.includes(category) && <IntegrationsContainer variant={category} />}
|
||||||
<IntegrationsContainer variant={category} />
|
{category === "calendar" && <CalendarListContainer />}
|
||||||
)}
|
{category === "other" && (
|
||||||
{(category === InstalledAppVariants.automation || category === InstalledAppVariants.analytics) && (
|
<IntegrationsContainer variant={category} exclude={[...categoryList, "calendar"]} />
|
||||||
<IntegrationsContainer variant={category} />
|
|
||||||
)}
|
|
||||||
{category === InstalledAppVariants.calendar && <CalendarListContainer />}
|
|
||||||
{category === InstalledAppVariants.other && (
|
|
||||||
<IntegrationsContainer
|
|
||||||
exclude={[
|
|
||||||
InstalledAppVariants.conferencing,
|
|
||||||
InstalledAppVariants.calendar,
|
|
||||||
InstalledAppVariants.analytics,
|
|
||||||
InstalledAppVariants.automation,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</InstalledAppsLayout>
|
</InstalledAppsLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default function Error() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<Link href="/auth/login" passHref>
|
<Link href="/auth/login" passHref legacyBehavior>
|
||||||
<Button className="flex w-full justify-center">{t("go_back_login")}</Button>
|
<Button className="flex w-full justify-center">{t("go_back_login")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
|
|||||||
<h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2>
|
<h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2>
|
||||||
</div>
|
</div>
|
||||||
<p>{t("request_is_expired_instructions")}</p>
|
<p>{t("request_is_expired_instructions")}</p>
|
||||||
<Link href="/auth/forgot-password" passHref>
|
<Link href="/auth/forgot-password" passHref legacyBehavior>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
|||||||
footerText={
|
footerText={
|
||||||
!success && (
|
!success && (
|
||||||
<>
|
<>
|
||||||
<Link href="/auth/login">
|
<Link href="/auth/login" className="font-medium text-gray-900">
|
||||||
<a className="font-medium text-neutral-900">{t("back_to_signin")}</a>
|
{t("back_to_signin")}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -115,7 +115,13 @@ export default function Login({
|
|||||||
description={t("login")}
|
description={t("login")}
|
||||||
showLogo
|
showLogo
|
||||||
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
|
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
|
||||||
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}>
|
footerText={
|
||||||
|
twoFactorRequired
|
||||||
|
? TwoFactorFooter
|
||||||
|
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
|
||||||
|
? LoginFooter
|
||||||
|
: null
|
||||||
|
}>
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form">
|
<form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form">
|
||||||
<div>
|
<div>
|
||||||
@@ -133,10 +139,11 @@ export default function Login({
|
|||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0">
|
<div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0">
|
||||||
<Link href="/auth/forgot-password">
|
<Link
|
||||||
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
|
href="/auth/forgot-password"
|
||||||
{t("forgot")}
|
tabIndex={-1}
|
||||||
</a>
|
className="text-sm font-medium text-gray-600">
|
||||||
|
{t("forgot")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<PasswordField
|
<PasswordField
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
{
|
{
|
||||||
title: t("enable_apps"),
|
title: t("enable_apps"),
|
||||||
description: t("enable_apps_description"),
|
description: t("enable_apps_description"),
|
||||||
content: <AdminAppsList baseURL="/auth/setup?step=2" />,
|
content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
||||||
import Schedule from "@calcom/features/schedules/components/Schedule";
|
import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { availabilityAsString } from "@calcom/lib/availability";
|
import { availabilityAsString } from "@calcom/lib/availability";
|
||||||
import { yyyymmdd } from "@calcom/lib/date-fns";
|
import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -16,7 +17,6 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
Icon,
|
Icon,
|
||||||
Label,
|
Label,
|
||||||
Shell,
|
|
||||||
showToast,
|
showToast,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
SkeletonText,
|
SkeletonText,
|
||||||
@@ -60,7 +60,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
|||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mb-4 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
|
<p className="mb-4 text-sm text-gray-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
|
||||||
<div className="mt-1 space-y-2">
|
<div className="mt-1 space-y-2">
|
||||||
<DateOverrideList
|
<DateOverrideList
|
||||||
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||||
@@ -186,11 +186,8 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
||||||
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
|
<div className="flex-1 divide-y divide-gray-200 rounded-md border">
|
||||||
<div className=" py-5 sm:p-6">
|
<div className=" py-5 sm:p-6">
|
||||||
<h3 className="mb-2 px-5 text-base font-medium leading-6 text-gray-900 sm:pl-0">
|
|
||||||
{t("change_start_end")}
|
|
||||||
</h3>
|
|
||||||
{typeof me.data?.weekStart === "string" && (
|
{typeof me.data?.weekStart === "string" && (
|
||||||
<Schedule
|
<Schedule
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { EmptyScreen, Icon, Shell, showToast } from "@calcom/ui";
|
import { EmptyScreen, Icon, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
import { withQuery } from "@lib/QueryCell";
|
import { withQuery } from "@lib/QueryCell";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
@@ -83,7 +84,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="mb-16 overflow-hidden rounded-md border border-gray-200 bg-white">
|
<div className="mb-16 overflow-hidden rounded-md border border-gray-200 bg-white">
|
||||||
<ul className="divide-y divide-neutral-200" data-testid="schedules" ref={animationParentRef}>
|
<ul className="divide-y divide-gray-200" data-testid="schedules" ref={animationParentRef}>
|
||||||
{schedules.map((schedule) => (
|
{schedules.map((schedule) => (
|
||||||
<ScheduleListItem
|
<ScheduleListItem
|
||||||
displayOptions={{
|
displayOptions={{
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { Shell, SkeletonText } from "@calcom/ui";
|
import { SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
if (e.target.value) setSelectedDate(e.target.value);
|
if (e.target.value) setSelectedDate(e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
|
<small className="block text-gray-400">{t("hover_over_bold_times_tip")}</small>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||||
@@ -77,17 +78,15 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
.map((slot: IBusySlot) => (
|
.map((slot: IBusySlot) => (
|
||||||
<div
|
<div
|
||||||
key={dayjs(slot.start).format("HH:mm")}
|
key={dayjs(slot.start).format("HH:mm")}
|
||||||
className="overflow-hidden rounded-md bg-neutral-100"
|
className="overflow-hidden rounded-md bg-gray-100"
|
||||||
data-testid="troubleshooter-busy-time">
|
data-testid="troubleshooter-busy-time">
|
||||||
<div className="px-4 py-5 text-black sm:p-6">
|
<div className="px-4 py-5 text-black sm:p-6">
|
||||||
{t("calendar_shows_busy_between")}{" "}
|
{t("calendar_shows_busy_between")}{" "}
|
||||||
<span
|
<span className="font-medium text-gray-800" title={dayjs(slot.start).format("HH:mm")}>
|
||||||
className="font-medium text-neutral-800"
|
|
||||||
title={dayjs(slot.start).format("HH:mm")}>
|
|
||||||
{dayjs(slot.start).format("HH:mm")}
|
{dayjs(slot.start).format("HH:mm")}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
{t("and")}{" "}
|
{t("and")}{" "}
|
||||||
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}>
|
<span className="font-medium text-gray-800" title={dayjs(slot.end).format("HH:mm")}>
|
||||||
{dayjs(slot.end).format("HH:mm")}
|
{dayjs(slot.end).format("HH:mm")}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
{t("on")} {dayjs(slot.start).format("D")}{" "}
|
{t("on")} {dayjs(slot.start).format("D")}{" "}
|
||||||
@@ -98,7 +97,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-md bg-neutral-100">
|
<div className="overflow-hidden rounded-md bg-gray-100">
|
||||||
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
|||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
import prisma, { baseUserSelect } from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { Prisma } from "@calcom/prisma/client";
|
import { Prisma } from "@calcom/prisma/client";
|
||||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Button, EmailInput, Icon, HeadSeo } from "@calcom/ui";
|
import { Button, EmailInput, Icon, HeadSeo } from "@calcom/ui";
|
||||||
@@ -322,7 +322,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
}
|
}
|
||||||
return t("emailed_you_and_attendees" + titleSuffix);
|
return t("emailed_you_and_attendees" + titleSuffix);
|
||||||
}
|
}
|
||||||
const userIsOwner = !!(session?.user?.id && eventType.users.find((user) => (user.id = session.user.id)));
|
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||||
useTheme(isSuccessBookingPage ? props.profile.theme : "light");
|
useTheme(isSuccessBookingPage ? props.profile.theme : "light");
|
||||||
const title = t(
|
const title = t(
|
||||||
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
|
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
|
||||||
@@ -353,10 +353,10 @@ export default function Success(props: SuccessProps) {
|
|||||||
)}
|
)}
|
||||||
{userIsOwner && !isEmbed && (
|
{userIsOwner && !isEmbed && (
|
||||||
<div className="mt-2 ml-4 -mb-4">
|
<div className="mt-2 ml-4 -mb-4">
|
||||||
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}>
|
<Link
|
||||||
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-transparent dark:hover:text-white">
|
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
|
||||||
<Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-transparent dark:hover:text-white">
|
||||||
</a>
|
<Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -379,7 +379,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
||||||
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border-neutral-700",
|
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 dark:border-darkgray-200 bg-white",
|
||||||
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
|
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
|
||||||
)}
|
)}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
@@ -408,7 +408,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-6 mb-8 text-center last:mb-0">
|
<div className="mt-6 mb-8 text-center last:mb-0">
|
||||||
<h3
|
<h3
|
||||||
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white"
|
className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white"
|
||||||
data-testid={isCancelled ? "cancelled-headline" : ""}
|
data-testid={isCancelled ? "cancelled-headline" : ""}
|
||||||
id="modal-headline">
|
id="modal-headline">
|
||||||
{needsConfirmation && !isCancelled
|
{needsConfirmation && !isCancelled
|
||||||
@@ -422,9 +422,10 @@ export default function Success(props: SuccessProps) {
|
|||||||
: t("meeting_is_scheduled")}
|
: t("meeting_is_scheduled")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<p className="text-neutral-600 dark:text-gray-300">{getTitle()}</p>
|
<p className="text-gray-600 dark:text-gray-300">{getTitle()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
|
|
||||||
|
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-200 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
|
||||||
{(isCancelled || reschedule) && cancellationReason && (
|
{(isCancelled || reschedule) && cancellationReason && (
|
||||||
<>
|
<>
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
@@ -499,7 +500,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
<>
|
<>
|
||||||
<div className="mt-9 font-medium">{t("additional_notes")}</div>
|
<div className="mt-9 font-medium">{t("additional_notes")}</div>
|
||||||
<div className="col-span-2 mb-2 mt-9">
|
<div className="col-span-2 mb-2 mt-9">
|
||||||
<p>{bookingInfo.description}</p>
|
<p className="break-words">{bookingInfo.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -573,7 +574,9 @@ export default function Success(props: SuccessProps) {
|
|||||||
{!props.recurringBookings && (
|
{!props.recurringBookings && (
|
||||||
<span className="text-bookinglight inline text-gray-700">
|
<span className="text-bookinglight inline text-gray-700">
|
||||||
<span className="underline">
|
<span className="underline">
|
||||||
<Link href={`/reschedule/${bookingInfo?.uid}`}>{t("reschedule")}</Link>
|
<Link href={`/reschedule/${bookingInfo?.uid}`} legacyBehavior>
|
||||||
|
{t("reschedule")}
|
||||||
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<span className="mx-2">{t("or_lowercase")}</span>
|
<span className="mx-2">{t("or_lowercase")}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -592,7 +595,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<hr className="border-bookinglightest dark:border-darkgray-300" />
|
<hr className="border-bookinglightest dark:border-darkgray-200" />
|
||||||
<CancelBooking
|
<CancelBooking
|
||||||
booking={{ uid: bookingInfo?.uid, title: bookingInfo?.title, id: bookingInfo?.id }}
|
booking={{ uid: bookingInfo?.uid, title: bookingInfo?.title, id: bookingInfo?.id }}
|
||||||
profile={{ name: props.profile.name, slug: props.profile.slug }}
|
profile={{ name: props.profile.name, slug: props.profile.slug }}
|
||||||
@@ -633,17 +636,16 @@ export default function Success(props: SuccessProps) {
|
|||||||
? "&recur=" +
|
? "&recur=" +
|
||||||
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
|
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
|
||||||
: "")
|
: "")
|
||||||
}>
|
}
|
||||||
<a className="h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-neutral-700 dark:text-white">
|
className="h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-gray-700 dark:text-white">
|
||||||
<svg
|
<svg
|
||||||
className="-mt-1.5 inline-block h-4 w-4"
|
className="-mt-1.5 inline-block h-4 w-4"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24">
|
viewBox="0 0 24 24">
|
||||||
<title>Google</title>
|
<title>Google</title>
|
||||||
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
|
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
@@ -657,19 +659,17 @@ export default function Success(props: SuccessProps) {
|
|||||||
"&subject=" +
|
"&subject=" +
|
||||||
eventName
|
eventName
|
||||||
) + (location ? "&location=" + location : "")
|
) + (location ? "&location=" + location : "")
|
||||||
}>
|
}
|
||||||
<a
|
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
|
||||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
target="_blank">
|
||||||
target="_blank">
|
<svg
|
||||||
<svg
|
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 24 24">
|
||||||
viewBox="0 0 24 24">
|
<title>Microsoft Outlook</title>
|
||||||
<title>Microsoft Outlook</title>
|
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
|
||||||
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
|
</svg>
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
@@ -683,34 +683,31 @@ export default function Success(props: SuccessProps) {
|
|||||||
"&subject=" +
|
"&subject=" +
|
||||||
eventName
|
eventName
|
||||||
) + (location ? "&location=" + location : "")
|
) + (location ? "&location=" + location : "")
|
||||||
}>
|
}
|
||||||
<a
|
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
|
||||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
target="_blank">
|
||||||
target="_blank">
|
<svg
|
||||||
<svg
|
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
fill="currentColor"
|
||||||
fill="currentColor"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
viewBox="0 0 24 24">
|
||||||
viewBox="0 0 24 24">
|
<title>Microsoft Office</title>
|
||||||
<title>Microsoft Office</title>
|
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
|
||||||
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
|
</svg>
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={"data:text/calendar," + eventLink()}>
|
<Link
|
||||||
<a
|
href={"data:text/calendar," + eventLink()}
|
||||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
|
||||||
download={props.eventType.title + ".ics"}>
|
download={props.eventType.title + ".ics"}>
|
||||||
<svg
|
<svg
|
||||||
version="1.1"
|
version="1.1"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 1000 1000"
|
viewBox="0 0 1000 1000"
|
||||||
className="mr-1 -mt-1.5 inline-block h-4 w-4">
|
className="mr-1 -mt-1.5 inline-block h-4 w-4">
|
||||||
<title>{t("other")}</title>
|
<title>{t("other")}</title>
|
||||||
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
|
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -849,6 +846,17 @@ export function RecurringBookings({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getEventTypesFromDB = async (id: number) => {
|
const getEventTypesFromDB = async (id: number) => {
|
||||||
|
const userSelect = {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
username: true,
|
||||||
|
hideBranding: true,
|
||||||
|
theme: true,
|
||||||
|
brandColor: true,
|
||||||
|
darkBrandColor: true,
|
||||||
|
email: true,
|
||||||
|
timeZone: true,
|
||||||
|
};
|
||||||
const eventType = await prisma.eventType.findUnique({
|
const eventType = await prisma.eventType.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
@@ -867,17 +875,17 @@ const getEventTypesFromDB = async (id: number) => {
|
|||||||
locations: true,
|
locations: true,
|
||||||
price: true,
|
price: true,
|
||||||
currency: true,
|
currency: true,
|
||||||
|
owner: {
|
||||||
|
select: userSelect,
|
||||||
|
},
|
||||||
users: {
|
users: {
|
||||||
|
select: userSelect,
|
||||||
|
},
|
||||||
|
hosts: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
user: {
|
||||||
name: true,
|
select: userSelect,
|
||||||
username: true,
|
},
|
||||||
hideBranding: true,
|
|
||||||
theme: true,
|
|
||||||
brandColor: true,
|
|
||||||
darkBrandColor: true,
|
|
||||||
email: true,
|
|
||||||
timeZone: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
@@ -921,6 +929,7 @@ const schema = z.object({
|
|||||||
uid: z.string(),
|
uid: z.string(),
|
||||||
email: z.string().optional(),
|
email: z.string().optional(),
|
||||||
eventTypeSlug: z.string().optional(),
|
eventTypeSlug: z.string().optional(),
|
||||||
|
cancel: stringToBoolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSeatsEventTypeOnBooking = (
|
const handleSeatsEventTypeOnBooking = (
|
||||||
@@ -954,7 +963,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const parsedQuery = schema.safeParse(context.query);
|
const parsedQuery = schema.safeParse(context.query);
|
||||||
if (!parsedQuery.success) return { notFound: true };
|
if (!parsedQuery.success) return { notFound: true };
|
||||||
const { uid, email, eventTypeSlug } = parsedQuery.data;
|
const { uid, email, eventTypeSlug, cancel } = parsedQuery.data;
|
||||||
|
|
||||||
const bookingInfo = await prisma.booking.findFirst({
|
const bookingInfo = await prisma.booking.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -1017,27 +1026,18 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eventTypeRaw.users.length && eventTypeRaw.userId) {
|
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
|
||||||
// TODO we should add `user User` relation on `EventType` so this extra query isn't needed
|
? eventTypeRaw.hosts.map((host) => host.user)
|
||||||
const user = await prisma.user.findUnique({
|
: eventTypeRaw.users;
|
||||||
where: {
|
|
||||||
id: eventTypeRaw.userId,
|
|
||||||
},
|
|
||||||
select: baseUserSelect,
|
|
||||||
});
|
|
||||||
if (user) {
|
|
||||||
eventTypeRaw.users.push({
|
|
||||||
...user,
|
|
||||||
avatar: "",
|
|
||||||
allowDynamicBooking: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!eventTypeRaw.users.length) {
|
if (!eventTypeRaw.users.length) {
|
||||||
return {
|
if (!eventTypeRaw.owner)
|
||||||
notFound: true,
|
return {
|
||||||
};
|
notFound: true,
|
||||||
|
};
|
||||||
|
eventTypeRaw.users.push({
|
||||||
|
...eventTypeRaw.owner,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventType = {
|
const eventType = {
|
||||||
@@ -1049,7 +1049,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl) {
|
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: eventType.successRedirectUrl,
|
destination: eventType.successRedirectUrl,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PeriodType, SchedulingType } from "@prisma/client";
|
import { PeriodType, SchedulingType } from "@prisma/client";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -15,6 +14,7 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
|||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
import getEventTypeById from "@calcom/lib/getEventTypeById";
|
import getEventTypeById from "@calcom/lib/getEventTypeById";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
@@ -64,7 +64,6 @@ export type FormValues = {
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
}[];
|
}[];
|
||||||
customInputs: CustomInputParsed[];
|
customInputs: CustomInputParsed[];
|
||||||
users: string[];
|
|
||||||
schedule: number;
|
schedule: number;
|
||||||
periodType: PeriodType;
|
periodType: PeriodType;
|
||||||
periodDays: number;
|
periodDays: number;
|
||||||
@@ -85,6 +84,8 @@ export type FormValues = {
|
|||||||
};
|
};
|
||||||
successRedirectUrl: string;
|
successRedirectUrl: string;
|
||||||
bookingLimits?: BookingLimit;
|
bookingLimits?: BookingLimit;
|
||||||
|
hosts: { userId: number }[];
|
||||||
|
hostsFixed: { userId: number }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomInputParsed = typeof customInputSchema._output;
|
export type CustomInputParsed = typeof customInputSchema._output;
|
||||||
@@ -111,15 +112,15 @@ export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
|
|||||||
const EventTypePage = (props: EventTypeSetupProps) => {
|
const EventTypePage = (props: EventTypeSetupProps) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const router = useRouter();
|
const {
|
||||||
const { tabName } = querySchema.parse(router.query);
|
data: { tabName },
|
||||||
|
} = useTypedQuery(querySchema);
|
||||||
|
|
||||||
const { data: eventTypeApps } = trpc.viewer.apps.useQuery({
|
const { data: eventTypeApps } = trpc.viewer.apps.useQuery({
|
||||||
extendsFeature: "EventType",
|
extendsFeature: "EventType",
|
||||||
});
|
});
|
||||||
|
|
||||||
const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props;
|
const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props;
|
||||||
|
|
||||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||||
@@ -185,6 +186,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
description: eventType.description ?? undefined,
|
description: eventType.description ?? undefined,
|
||||||
schedule: eventType.schedule || undefined,
|
schedule: eventType.schedule || undefined,
|
||||||
bookingLimits: eventType.bookingLimits || undefined,
|
bookingLimits: eventType.bookingLimits || undefined,
|
||||||
|
length: eventType.length,
|
||||||
hidden: eventType.hidden,
|
hidden: eventType.hidden,
|
||||||
periodDates: {
|
periodDates: {
|
||||||
startDate: periodDates.startDate,
|
startDate: periodDates.startDate,
|
||||||
@@ -194,12 +196,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
||||||
schedulingType: eventType.schedulingType,
|
schedulingType: eventType.schedulingType,
|
||||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||||
minimumBookingNoticeInDurationType: convertToNewDurationType(
|
|
||||||
"minutes",
|
|
||||||
findDurationType(eventType.minimumBookingNotice),
|
|
||||||
eventType.minimumBookingNotice
|
|
||||||
),
|
|
||||||
metadata,
|
metadata,
|
||||||
|
hosts: !!eventType.hosts?.length
|
||||||
|
? eventType.hosts.filter((host) => !host.isFixed)
|
||||||
|
: eventType.users
|
||||||
|
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
|
||||||
|
.map((user) => ({ userId: user.id })),
|
||||||
|
hostsFixed: !!eventType.hosts?.length
|
||||||
|
? eventType.hosts.filter((host) => host.isFixed)
|
||||||
|
: eventType.users
|
||||||
|
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
|
||||||
|
.map((user) => ({ userId: user.id })),
|
||||||
},
|
},
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
z
|
z
|
||||||
@@ -237,7 +244,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
availability: <AvailabilityTab isTeamEvent={!!team} />,
|
availability: <AvailabilityTab isTeamEvent={!!team} />,
|
||||||
team: <EventTeamTab eventType={eventType} teamMembers={teamMembers} team={team} />,
|
team: <EventTeamTab teamMembers={teamMembers} team={team} />,
|
||||||
limits: <EventLimitsTab eventType={eventType} />,
|
limits: <EventLimitsTab eventType={eventType} />,
|
||||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||||
recurring: <EventRecurringTab eventType={eventType} />,
|
recurring: <EventRecurringTab eventType={eventType} />,
|
||||||
@@ -278,6 +285,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
locations,
|
locations,
|
||||||
metadata,
|
metadata,
|
||||||
customInputs,
|
customInputs,
|
||||||
|
hosts: hostsInput,
|
||||||
|
hostsFixed,
|
||||||
// We don't need to send send these values to the backend
|
// We don't need to send send these values to the backend
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
seatsPerTimeSlotEnabled,
|
seatsPerTimeSlotEnabled,
|
||||||
@@ -286,6 +295,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
...input
|
...input
|
||||||
} = values;
|
} = values;
|
||||||
|
|
||||||
|
const hosts: (typeof hostsInput[number] & { isFixed?: boolean })[] = [];
|
||||||
|
if (hostsInput || hostsFixed) {
|
||||||
|
hosts.push(...hostsInput.concat(hostsFixed.map((host) => ({ isFixed: true, ...host }))));
|
||||||
|
}
|
||||||
|
|
||||||
if (bookingLimits) {
|
if (bookingLimits) {
|
||||||
const isValid = validateBookingLimitOrder(bookingLimits);
|
const isValid = validateBookingLimitOrder(bookingLimits);
|
||||||
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
|
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
|
||||||
@@ -303,6 +317,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
|
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
...input,
|
...input,
|
||||||
|
hosts,
|
||||||
locations,
|
locations,
|
||||||
recurringEvent,
|
recurringEvent,
|
||||||
periodStartDate: periodDates.startDate,
|
periodStartDate: periodDates.startDate,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CreateEventTypeButton,
|
CreateEventTypeButton,
|
||||||
EventTypeDescriptionLazy as EventTypeDescription,
|
EventTypeDescriptionLazy as EventTypeDescription,
|
||||||
} from "@calcom/features/eventtypes/components";
|
} from "@calcom/features/eventtypes/components";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||||
@@ -26,9 +27,10 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
Icon,
|
Icon,
|
||||||
Shell,
|
|
||||||
showToast,
|
showToast,
|
||||||
Switch,
|
Switch,
|
||||||
|
Avatar,
|
||||||
|
AvatarGroup,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
@@ -37,8 +39,6 @@ import { HttpError } from "@lib/core/http/error";
|
|||||||
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
||||||
import Avatar from "@components/ui/Avatar";
|
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
@@ -64,27 +64,26 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/event-types/${type.id}?tabName=setup`}>
|
<Link
|
||||||
<a
|
href={`/event-types/${type.id}?tabName=setup`}
|
||||||
className="flex-1 overflow-hidden pr-4 text-sm"
|
className="flex-1 overflow-hidden pr-4 text-sm"
|
||||||
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1"
|
className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1"
|
||||||
data-testid={"event-type-title-" + type.id}>
|
data-testid={"event-type-title-" + type.id}>
|
||||||
{type.title}
|
{type.title}
|
||||||
|
</span>
|
||||||
|
<small
|
||||||
|
className="hidden font-normal leading-4 text-gray-600 sm:inline"
|
||||||
|
data-testid={"event-type-slug-" + type.id}>{`/${group.profile.slug}/${type.slug}`}</small>
|
||||||
|
{readOnly && (
|
||||||
|
<span className="rtl:ml-2inline items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2 ltr:mr-2">
|
||||||
|
{t("readonly")}
|
||||||
</span>
|
</span>
|
||||||
<small
|
)}
|
||||||
className="hidden font-normal leading-4 text-gray-600 sm:inline"
|
</div>
|
||||||
data-testid={"event-type-slug-" + type.id}>{`/${group.profile.slug}/${type.slug}`}</small>
|
<EventTypeDescription eventType={type} />
|
||||||
{readOnly && (
|
|
||||||
<span className="rtl:ml-2inline items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2 ltr:mr-2">
|
|
||||||
{t("readonly") as string}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<EventTypeDescription eventType={type} />
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -252,7 +251,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
const lastItem = types[types.length - 1];
|
const lastItem = types[types.length - 1];
|
||||||
return (
|
return (
|
||||||
<div className="mb-16 flex overflow-hidden rounded-md border border-gray-200 bg-white">
|
<div className="mb-16 flex overflow-hidden rounded-md border border-gray-200 bg-white">
|
||||||
<ul ref={parent} className="!static w-full divide-y divide-neutral-200" data-testid="event-types">
|
<ul ref={parent} className="!static w-full divide-y divide-gray-200" data-testid="event-types">
|
||||||
{types.map((type, index) => {
|
{types.map((type, index) => {
|
||||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||||
const calLink = `${CAL_URL}/${embedLink}`;
|
const calLink = `${CAL_URL}/${embedLink}`;
|
||||||
@@ -280,9 +279,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||||
{type.users?.length > 1 && (
|
{type.users?.length > 1 && (
|
||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
border="border-2 border-white"
|
|
||||||
className="relative top-1 right-3"
|
className="relative top-1 right-3"
|
||||||
size={8}
|
size="sm"
|
||||||
truncateAfter={4}
|
truncateAfter={4}
|
||||||
items={type.users.map((organizer) => ({
|
items={type.users.map((organizer) => ({
|
||||||
alt: organizer.name || "",
|
alt: organizer.name || "",
|
||||||
@@ -297,7 +295,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
{t("hidden")}
|
{t("hidden")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={t("show_eventtype_on_profile") as string}>
|
<Tooltip content={t("show_eventtype_on_profile")}>
|
||||||
<div className="self-center rounded-md p-2 hover:bg-gray-200">
|
<div className="self-center rounded-md p-2 hover:bg-gray-200">
|
||||||
<Switch
|
<Switch
|
||||||
name="Hidden"
|
name="Hidden"
|
||||||
@@ -310,20 +308,20 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<ButtonGroup combined>
|
<ButtonGroup combined>
|
||||||
<Tooltip content={t("preview") as string}>
|
<Tooltip content={t("preview")}>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
size="icon"
|
variant="icon"
|
||||||
href={calLink}
|
href={calLink}
|
||||||
StartIcon={Icon.FiExternalLink}
|
StartIcon={Icon.FiExternalLink}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content={t("copy_link") as string}>
|
<Tooltip content={t("copy_link")}>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="icon"
|
variant="icon"
|
||||||
StartIcon={Icon.FiLink}
|
StartIcon={Icon.FiLink}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
@@ -338,7 +336,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
className="radix-state-open:rounded-r-md">
|
className="radix-state-open:rounded-r-md">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="icon"
|
variant="icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
StartIcon={Icon.FiMoreHorizontal}
|
StartIcon={Icon.FiMoreHorizontal}
|
||||||
/>
|
/>
|
||||||
@@ -350,7 +348,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
data-testid={"event-type-edit-" + type.id}
|
data-testid={"event-type-edit-" + type.id}
|
||||||
StartIcon={Icon.FiEdit2}
|
StartIcon={Icon.FiEdit2}
|
||||||
onClick={() => router.push("/event-types/" + type.id)}>
|
onClick={() => router.push("/event-types/" + type.id)}>
|
||||||
{t("edit") as string}
|
{t("edit")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
@@ -359,7 +357,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
data-testid={"event-type-duplicate-" + type.id}
|
data-testid={"event-type-duplicate-" + type.id}
|
||||||
StartIcon={Icon.FiCopy}
|
StartIcon={Icon.FiCopy}
|
||||||
onClick={() => openDuplicateModal(type)}>
|
onClick={() => openDuplicateModal(type)}>
|
||||||
{t("duplicate") as string}
|
{t("duplicate")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
@@ -372,18 +370,19 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
{t("embed")}
|
{t("embed")}
|
||||||
</EmbedButton>
|
</EmbedButton>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
<DropdownMenuSeparator />
|
||||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||||
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
|
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
color="destructive"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
setDeleteDialogTypeId(type.id);
|
setDeleteDialogTypeId(type.id);
|
||||||
}}
|
}}
|
||||||
StartIcon={Icon.FiTrash}
|
StartIcon={Icon.FiTrash}
|
||||||
className="w-full rounded-none">
|
className="w-full rounded-none">
|
||||||
{t("delete") as string}
|
{t("delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
@@ -397,20 +396,23 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<div className="min-w-9 mx-5 flex sm:hidden">
|
<div className="min-w-9 mx-5 flex sm:hidden">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
|
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
|
||||||
<Button type="button" size="icon" color="secondary" StartIcon={Icon.FiMoreHorizontal} />
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="icon"
|
||||||
|
color="secondary"
|
||||||
|
StartIcon={Icon.FiMoreHorizontal}
|
||||||
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<Link href={calLink}>
|
<Link href={calLink} target="_blank">
|
||||||
<a target="_blank">
|
<Button
|
||||||
<Button
|
color="minimal"
|
||||||
color="minimal"
|
StartIcon={Icon.FiExternalLink}
|
||||||
StartIcon={Icon.FiExternalLink}
|
className="w-full rounded-none">
|
||||||
className="w-full rounded-none">
|
{t("preview")}
|
||||||
{t("preview") as string}
|
</Button>
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
@@ -424,7 +426,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
navigator.clipboard.writeText(calLink);
|
navigator.clipboard.writeText(calLink);
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
}}>
|
}}>
|
||||||
{t("copy_link") as string}
|
{t("copy_link")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{isNativeShare ? (
|
{isNativeShare ? (
|
||||||
@@ -445,7 +447,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
.then(() => showToast(t("link_shared"), "success"))
|
.then(() => showToast(t("link_shared"), "success"))
|
||||||
.catch(() => showToast(t("failed"), "error"));
|
.catch(() => showToast(t("failed"), "error"));
|
||||||
}}>
|
}}>
|
||||||
{t("share") as string}
|
{t("share")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -456,7 +458,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
color="minimal"
|
color="minimal"
|
||||||
className="w-full rounded-none"
|
className="w-full rounded-none"
|
||||||
StartIcon={Icon.FiEdit}>
|
StartIcon={Icon.FiEdit}>
|
||||||
{t("edit") as string}
|
{t("edit")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
@@ -467,10 +469,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
data-testid={"event-type-duplicate-" + type.id}
|
data-testid={"event-type-duplicate-" + type.id}
|
||||||
StartIcon={Icon.FiCopy}
|
StartIcon={Icon.FiCopy}
|
||||||
onClick={() => openDuplicateModal(type)}>
|
onClick={() => openDuplicateModal(type)}>
|
||||||
{t("duplicate") as string}
|
{t("duplicate")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -480,7 +482,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
color="destructive"
|
color="destructive"
|
||||||
StartIcon={Icon.FiTrash}
|
StartIcon={Icon.FiTrash}
|
||||||
className="w-full rounded-none">
|
className="w-full rounded-none">
|
||||||
{t("delete") as string}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -502,7 +504,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteEventTypeHandler(deleteDialogTypeId);
|
deleteEventTypeHandler(deleteDialogTypeId);
|
||||||
}}>
|
}}>
|
||||||
{t("delete_event_type_description") as string}
|
{t("delete_event_type_description")}
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -515,38 +517,34 @@ const EventTypeListHeading = ({
|
|||||||
teamId,
|
teamId,
|
||||||
}: EventTypeListHeadingProps): JSX.Element => {
|
}: EventTypeListHeadingProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 flex">
|
<div className="mb-4 flex items-center space-x-2">
|
||||||
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
|
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
|
||||||
<a>
|
<Avatar
|
||||||
<Avatar
|
alt={profile?.name || ""}
|
||||||
alt={profile?.name || ""}
|
imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
|
||||||
imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
|
size="sm"
|
||||||
size={8}
|
className="mt-1 inline ltr:mr-2 rtl:ml-2"
|
||||||
className="mt-1 inline ltr:mr-2 rtl:ml-2"
|
/>
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
|
<Link
|
||||||
<a className="font-bold">{profile?.name || ""}</a>
|
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
|
||||||
|
className="font-bold">
|
||||||
|
{profile?.name || ""}
|
||||||
</Link>
|
</Link>
|
||||||
{membershipCount && teamId && (
|
{membershipCount && teamId && (
|
||||||
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 ltr:mr-2 rtl:ml-2">
|
<span className="relative -top-px text-xs text-gray-500 ltr:ml-2 ltr:mr-2 rtl:ml-2">
|
||||||
<Link href={`/settings/teams/${teamId}/members`}>
|
<Link href={`/settings/teams/${teamId}/members`}>
|
||||||
<a>
|
<Badge variant="gray">
|
||||||
<Badge variant="gray">
|
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
|
||||||
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
|
{membershipCount}
|
||||||
{membershipCount}
|
</Badge>
|
||||||
</Badge>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{profile?.slug && (
|
{profile?.slug && (
|
||||||
<Link href={`${CAL_URL}/${profile.slug}`}>
|
<Link href={`${CAL_URL}/${profile.slug}`} className="block text-xs text-gray-500">
|
||||||
<a className="block text-xs text-neutral-500">{`${CAL_URL?.replace("https://", "")}/${
|
{`${CAL_URL?.replace("https://", "")}/${profile.slug}`}
|
||||||
profile.slug
|
|
||||||
}`}</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -581,10 +579,7 @@ const EventTypesPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Shell
|
<Shell heading={t("event_types_page_title")} subtitle={t("event_types_page_subtitle")} CTA={<CTA />}>
|
||||||
heading={t("event_types_page_title") as string}
|
|
||||||
subtitle={t("event_types_page_subtitle") as string}
|
|
||||||
CTA={<CTA />}>
|
|
||||||
<WithQuery
|
<WithQuery
|
||||||
customLoader={<SkeletonLoader />}
|
customLoader={<SkeletonLoader />}
|
||||||
success={({ data }) => (
|
success={({ data }) => (
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
throw new Error("User from session not found");
|
throw new Error("User from session not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.completedOnboarding) {
|
||||||
|
return { redirect: { permanent: false, destination: "/event-types" } };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(context.locale ?? "", ["common"])),
|
...(await serverSideTranslations(context.locale ?? "", ["common"])),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { MobileNavigationMoreItems, Shell } from "@calcom/ui";
|
|
||||||
|
|
||||||
export default function MorePage() {
|
export default function MorePage() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|||||||
Loaded 100 of 636 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user