Compare commits

...
165 Commits
Author SHA1 Message Date
Keith Williams 36d47bd2d8 v2.8.3
release.yml / release (push) Canceled after 0s
2023-04-18 18:22:24 -03:00
26cdb6521f perf: Adds PageWrapper to pages (#8344)
* Adds PageWrapper to pages

* Add missing PageWrapper

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-04-18 15:45:32 -03:00
Leo Giovanetti 014f1986de Adding missing mutation (#8357)
* Adding missing mutation

* Returning to prop for disabled
2023-04-18 16:48:19 +00:00
Carina WollendorferandCarinaWolli 3ef328421f Date override fixes (#8330)
* fix date override for fixed round robin + time zone in date override

* check if slot is within working hours of fixed hosts

* add test for date override in different time zone

* fix date overrides for not fixed hosts (round robin)

* code clean up

* fix added test

* use the correct timezone of user for date overrides

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-18 17:21:27 +02:00
Ben HybertandHariom Balhara c4fe69d826 Add Plausible (Custom) App (#8189)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-04-18 19:24:51 +05:30
Hariom Balhara 6fd64b2209 Fix: cssVars API not working correctly on Booker Timeslot selection page (#8296)
* Use a method out of react scope to set CSS vars as that doesnt need react

* Refactor
2023-04-18 12:53:46 +00:00
Crowdin Bot 585e36793a New Crowdin translations by Github Action 2023-04-18 12:53:33 +00:00
Crowdin Bot 09b9370e13 New Crowdin translations by Github Action 2023-04-18 12:52:43 +00:00
Udit Takkar a76a4996b4 fix: address input (#8349)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-04-18 18:21:46 +05:30
Hariom Balhara b28d60b6dc API Support: Manage Booking Questions and responses (#8115) 2023-04-18 18:05:06 +05:30
Nafees Nazik a716eda14c fix: use the correct timezone of the user in booking detail page (#7915)
* fix: send timezone in query

* fiz: timezone

* fix: remove tz from createPayment

* fix: send timezone in query

* fiz: timezone

* fix: remove tz

* fix: remove tz

* fix: remove timezone query

* fix: timezone
2023-04-18 14:29:26 +02:00
Crowdin Bot 99f1524183 New Crowdin translations by Github Action 2023-04-18 12:10:14 +00:00
Peer Richelsen 7ce6454915 Update package.json
release.yml / release (push) Canceled after 0s
2023-04-18 14:09:25 +02:00
Crowdin Bot 93cb21f55a New Crowdin translations by Github Action 2023-04-18 10:09:05 +00:00
0aca769d06 Allow editing workflow templates (#8028)
* add event end time as variable

* add timezone as new variable

* add first version of template prefill

* set template body when template is updated

* set reminder template body and subject when creating workflow

* set email subject when changes templates

* save emailBody and emailsubject for all templates + fix duplicate template text

* add more flexibility for templates

* remove console.log

* fix {ORAGANIZER} and {ATTENDEE} variable

* make sure to always send reminder body and not default template

* fix import

* remove email body text and match variables in templates

* handle translations of formatted variables

* fix email reminder template

* add cancel and reschedule link as variable

* add cancel and reschedule link for scheduled emails/sms

* make sure empty empty body and subject are set for reminder template

* add info message for testing workflow

* fix typo

* add sms template

* add migration to remove reminderBody and emailSubject

* add branding

* code clean up

* add hide branding everywhere

* fix sms reminder template

* set sms reminder template if sms body is empty

* fix custom inputs variables everywhere

* fix variable translations + other small fixes

* fix some type errors

* fix more type errors

* fix everything missing around cron job scheduling

* make sure to always use custom template for sms messages

* fix type error

* code clean up

* rename link to url

* Add debug logs

* Update handleNewBooking.ts

* Add debug logs

* removed unneded responses

* fix booking questions + UI improvements

* remove html email body when changing to sms action

* code clean up + comments

* code clean up

* code clean up

* remove comment

* more clear info message for timezone variable

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-04-18 11:08:09 +01:00
Nafees Nazik c0bda6257e Fix: dark mode issues (#8348)
* fix: dark mode

* fix: dark mode
2023-04-18 09:12:12 +00:00
Yagiz Nizipliandzomars ae22246f05 perf: improve application toggle flow (#8302)
* perf: improve application toggle flow

* Reverses logic for enabled apps

---------

Co-authored-by: zomars <zomars@me.com>
2023-04-18 01:17:16 -07:00
Crowdin Bot e2931d9fa2 New Crowdin translations by Github Action 2023-04-18 07:54:34 +00:00
Crowdin Bot e6a8a31b53 New Crowdin translations by Github Action 2023-04-18 07:53:41 +00:00
Crowdin Bot ee86c751f7 New Crowdin translations by Github Action 2023-04-18 07:52:53 +00:00
Crowdin Bot 073b212c05 New Crowdin translations by Github Action 2023-04-18 07:52:00 +00:00
Nafees NazikandBailey Pumfleet cd8e2124c4 fix: check box (#8328)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-04-18 08:51:03 +01:00
JoeandOmar López 9dd36bd20e fix: signup form submiting after api error (#8298)
Co-authored-by: Omar López <zomars@me.com>
2023-04-18 01:58:54 +00:00
Crowdin Bot b1404356ff New Crowdin translations by Github Action 2023-04-18 01:55:59 +00:00
Nafees Nazikandzomars f467bb7957 perf: use LRU cache for getServerSession (#8339)
* chore: add lru package

* feat: use LRU cache

* fix: type

* Updates package json

---------

Co-authored-by: zomars <zomars@me.com>
2023-04-18 01:55:01 +00:00
597b14cf4c perf: improve application list execution (#8301)
* perf: improve application list execution

* Removed unused interface

* Type fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-18 00:54:15 +00:00
2bdd6df755 avoid calendar cache on cold starts (#8311)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-04-17 23:56:08 +00:00
Yagiz NizipliandEfraín Rochín ad958f10bd perf: improve booking confirm authorization (#8304)
* perf: improve booking confirm authorization

* removes unreachable code

---------

Co-authored-by: Efraín Rochín <roae.85@gmail.com>
2023-04-17 23:50:34 +00:00
e5eb7c3906 perf: use getSlimServerSession for session retrieval (#8308)
* perf: use getSlimServerSession for session retrieval

Creates a new `getSlimServerSession` method and replaces all calls to `getServerSession`.

The new method is much faster than `getServerSession` with it not requiring the NextAuth options object which includes a number of additional packages.

Additionally introduces a primitive in-memory cache for slim sessions.

* fix: account for seconds in token.exp

* Reverts diffs

* Replaces getServerSession with slimmer version

* Update disable.ts

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-17 23:48:00 +00:00
zomars 667d42d5da Update yarn.lock 2023-04-17 15:46:43 -07:00
zomars a23a9ad15a Reverts PR checks limit 2023-04-17 15:26:17 -07:00
Crowdin Bot 4482ffc66b New Crowdin translations by Github Action 2023-04-17 22:23:34 +00:00
Udit Takkar 493efabd2e perf: remove sentry (#8334) 2023-04-17 22:22:44 +00:00
Crowdin Bot a0355e276d New Crowdin translations by Github Action 2023-04-17 19:55:05 +00:00
b5c993e153 perf: replace notIn with equals on booking (#8303)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-04-17 16:54:14 -03:00
Crowdin Bot da3ee3f1fe New Crowdin translations by Github Action 2023-04-17 16:20:34 +00:00
Udit TakkarandPeer Richelsen d9fc595444 fix: undo showRecordingsButton (#8262)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-17 16:19:24 +00:00
06d13e0f6e feat: Added option to select the exchange server version (#7908)
* Added option to select the exchange server version

* Fixed linting issues

* Removed yarn changes

* Update packages/app-store/exchangecalendar/lib/CalendarService.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Removed requested change

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-17 10:21:14 -04:00
Udit Takkar 7dc3266133 fix: use text-emphasis (#8332)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-04-17 14:54:39 +01:00
Hariom Balhara 2c96444058 Fix/Keep themes in peace across embed and booking pages and App (#8108) 2023-04-17 17:46:54 +05:30
Nafees Nazik 39c48e7f79 fix: some ui regressions (#8260)
* fix: icon size

* fix: icon size

* fix: border
2023-04-17 12:09:30 +00:00
Hariom Balhara b6e1989f17 Invalidate the form query on toggle to update toggle state in UI (#8325) 2023-04-17 17:34:25 +05:30
Nafees Nazik 45b5b2a376 fix: giphy input dark color 2023-04-17 17:27:30 +05:30
JoeandPeer Richelsen 05726ec94c fix: varients to layer (#8171)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-16 22:22:10 +00:00
Peer RichelsenandAlex van Andel 6340ff43d8 removed page_view tracker (#8267)
* removed page_view tracker

* Removed unused useTelemetry

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-16 21:58:47 +00:00
Peer Richelsen e64b26c7e1 fixed dark mode booker (#8315) 2023-04-16 21:47:07 +00:00
Joe c829a7de82 fix: remove line-clamp from plugins (#8309) 2023-04-16 23:05:44 +02:00
JoeandPeer Richelsen 0205bfb066 fix: MobileSettingsContainer height (#8295)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-16 20:18:56 +00:00
5fd7d63388 correction of position on 'Adding events to' button (#8290)
* correction to position

* Update apps/web/components/settings/CalendarSwitch.tsx

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-16 20:06:02 +00:00
e1ea2cb532 chore: UI username RTL (#8300)
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-16 19:35:17 +00:00
Joe 88d151aa35 fix: scroll-bar class for editor compnent (#8310) 2023-04-16 21:17:07 +02:00
Crowdin Bot 4e7c93a322 New Crowdin translations by Github Action 2023-04-16 18:50:25 +00:00
Peer Richelsen 281c7628c1 fixed bg 2023-04-16 20:49:12 +02:00
Crowdin Bot 1b24bc14cd New Crowdin translations by Github Action 2023-04-15 13:23:37 +00:00
Hariom Balhara be3d2fca05 Use name to generate unique for attribute for label (#8265) 2023-04-15 13:22:51 +00:00
Crowdin Bot 327e11e7c7 New Crowdin translations by Github Action 2023-04-15 11:51:04 +00:00
Peer Richelsen bd999345f4 Update package.json
release.yml / release (push) Canceled after 0s
2023-04-15 13:50:06 +02:00
Keith Williams 081776b4f9 Fixing syntax error in stale issue cron action (#8269) 2023-04-15 13:49:16 +02:00
Peer Richelsen 8c328ce5d7 fixed yarn dx (#8293) 2023-04-15 11:48:44 +00:00
Crowdin Bot 0fa2a6aea3 New Crowdin translations by Github Action
release.yml / release (push) Canceled after 0s
2023-04-15 09:21:02 +00:00
Peer Richelsen 440251f9c3 Update package.json 2023-04-15 11:20:06 +02:00
Omar LópezandLeo Giovanetti 7f6847f7c7 In-app users admin (#8035)
* WIP

* WIP

* Update trpc-router.ts

* Fixes

* Update trpc-router.ts

* Update zod-utils.ts

* Apply suggestions from code review

Co-authored-by: Leo Giovanetti <hello@leog.me>

* Feedback and dark mode fixes

* Feedback

* Fixes

* Update UserForm.tsx

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-04-14 17:04:48 -07:00
zomars 9f1fa83a97 Update pr.yml 2023-04-14 17:04:35 -07:00
Joe Au-Yeung f00b112792 Change Stripe <CardElement /> to <PaymentElement /> (#8268)
* Use Stripe PaymentElement

* Only show needs confirmation, if the booking needs it

* Clean up

* Type fix

* More type fixes
2023-04-14 21:56:16 +00:00
Yagiz Nizipli 5339a489f5 perf: reduce array iteration on bookings router (#8276) 2023-04-14 21:29:43 +00:00
Yagiz Nizipli f7df204e0b perf: parallelize database calls to improve speed (#8275) 2023-04-14 21:27:10 +00:00
Alex van Andel 4975aba5e0 Move some dependencies into async (#8270)
* Move some dependencies into async

* Also remove jackson & jose from global scope
2023-04-14 14:25:15 -07:00
Omar López 29d61ad67b Update CODEOWNERS 2023-04-14 14:18:06 -07:00
Udit Takkar 6e555e64fa fix: spacing in booking item mobile (#8279)
release.yml / release (push) Canceled after 0s
2023-04-14 22:35:17 +02:00
Peer Richelsen c748bbc7e6 Update package.json 2023-04-14 22:34:32 +02:00
Leo Giovanetti 0681359bf3 Removing unneeded endpoint (#8280) 2023-04-14 20:27:37 +00:00
Yagiz Nizipli 4c9d4d7972 refactor: remove unnecessary or operation (#8274) 2023-04-14 11:03:24 -07:00
Crowdin Bot 67f8de42b3 New Crowdin translations by Github Action 2023-04-14 11:00:31 +00:00
Peer Richelsen 87584132b5 Update package.json
release.yml / release (push) Canceled after 0s
2023-04-14 12:59:31 +02:00
Crowdin Bot 5b943742e0 New Crowdin translations by Github Action 2023-04-14 08:05:36 +00:00
Hariom Balhara fe155b0b97 Fix: Sylaps app shown uninstalled after installation (#8256) 2023-04-14 10:04:41 +02:00
Udit TakkarandOmar López 6639588c2d fix: selectfield bug and min height (#8253)
* fix: selectfield bug and min height

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: remove overflow

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Omar López <zomars@me.com>
2023-04-13 21:45:18 +00:00
zomars 7f9e8ca7a6 Runs pipelines only when a PR has a requested review 2023-04-13 14:24:26 -07:00
e478a46358 Feature: Reserve slots currently being booked (#6909)
* Reserving slot picked up on cache

* change memory-cache to database table to block slots while reservation completes

* remove memory-cache

* update realeaseAt field when same user change te selected Slot

* Change default time to book

Co-authored-by: alannnc <alannnc@gmail.com>

* remove ip field and renews the session when the user remains in the booking form

* Remove duplicate router

* types fixes

* nit picks

* Update turbo.json

* Revert unrelated change

* Uses constant

* Constant already has a fallback

* Update slots.ts

* Unit test fixes

* slot reservation on user level and support seats

* types fixes and reserve slots on click

* Fix nit var name

---------

Co-authored-by: Efraín Rochín <roae.85@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-13 19:55:26 +00:00
Udit Takkar 7c9012738a feat: add isRecordingExist field and create api handler (#6777) 2023-04-13 19:07:10 +00:00
alannnc 5c763389f7 Fix workflows on seated events (#8034)
* Fix workflows on seated events

* Fix new args required in handleConfirmation

* Update function import to new name

* added email to override for workflows
2023-04-13 19:03:08 +00:00
Efraín Rochín 7fde30edf9 avoid get error when a user has no schedule defined in getUserAvailability function (#8170) 2023-04-13 18:26:45 +00:00
Hariom Balharaandsean-brydon 0e22e92cab Feat: Support setting custom css variables through embed API. (#8169)
* Add booker border specific custom variables

* Support configuring cssVars through embed API

* Update apps/web/pages/booking/[uid].tsx

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-04-13 18:26:31 +00:00
JoeandPeer Richelsen 17f48614ff fix: reccuring input width (#8227)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-13 18:26:13 +00:00
zomars 2f33b57eb7 Update yarn.lock 2023-04-13 11:12:25 -07:00
Nafees Nazik 6e5c3a88ee fix: clock icon size in booker page (#8249) 2023-04-13 20:08:09 +02:00
Alex van Andel 9024914f23 v2.7.16
release.yml / release (push) Canceled after 0s
2023-04-13 19:41:45 +02:00
sean-brydon 966bc08eb4 Fix radio item on future limits (#8247) 2023-04-13 11:58:08 +00:00
sean-brydon c545331cfe Few darkmode fixes - Avatar Tooltip | Forgot password page (#8242) 2023-04-13 10:59:32 +00:00
Peer Richelsen ce763f30f6 readded vibrancy for desktop app (#8245) 2023-04-13 10:59:09 +00:00
Peer Richelsen d453ae9f2b use brand for highlight (#8240) 2023-04-13 11:41:12 +01:00
Nafees Nazik 9f2b41e597 fix: Dark mode fixes (#8243)
* fix: event type card

* fix: text

* fix: border
2023-04-13 10:34:44 +01:00
GitStart-Cal.com 86e903ce4a show the link only if there's a username (#8238) 2023-04-13 07:04:52 +00:00
Crowdin Bot 9d3963436b New Crowdin translations by Github Action 2023-04-13 05:28:45 +00:00
alannnc d8218a6243 Fix payment app credential search (#8235) 2023-04-13 10:57:54 +05:30
Crowdin Bot cdc3833e09 New Crowdin translations by Github Action 2023-04-13 02:12:21 +00:00
Crowdin Bot 52b7e401a1 New Crowdin translations by Github Action 2023-04-13 02:11:27 +00:00
5170fc2424 Managed event-types (#6876)
* WIP

* Locked fields manager

* Leftovers

* Bad merge fix

* Type import fix

* Moving away from classes

* Progress refactoring locked logic

* Covering apps, webhooks and workflows

* Supporting webhooks and workflows (TBT)

* Restoring yarn.lock

* Progress

* Refactoring code, adding default values

* Fixing CRUD for children

* Connect app link and case-sensitive lib renaming

* Translation missing

* Locked indicators, empty screens, locations

* Member card and hidden status + missing i18n

* Missing existent children shown

* Showing preview for already created children

* Email notification almost in place

* Making progress over notif email

* Fixing nodemailer by mixed FE/BE mixup

* Delete dialog

* Adding tests

* New test

* Reverting unneeded change

* Removed console.log

* Tweaking email

* Reverting not applicable webhook changes

* Reverting dev email api

* Fixing last changes due to tests

* Changing user-evType relationship

* Availability and slug replacement tweaks

* Fixing event type delete

* Sometimes slug is not there...

* Removing old webhooks references
Changed slug hint

* Fixing types

* Fixing hiding event types actions

* Changing delete dialog text

* Removing unneeded code

* Applying feedback

* Update yarn.lock

* Making sure locked fields values are static

* Applying feedback

* Feedback + relying on children list, not users

* Removing console.log

* PR Feedback

* Telemetry for slug replacement action

* More unit tests

* Relying on schedule and editor tweaks

* Fixing conteiner classname

* PR Feedback

* PR Feedback

* Updating unit tests

* Moving stuff to ee, added feature flag

* type fix

* Including e2e

* Reverting unneeded changes in EmptyScreen

* Fixing some UI issues after merging tokens

* Fixing missing disabled locked fields

* Theme fixes + e2e potential fix

* Fixing e2e

* Fixing login relying on network

* Tweaking e2e

* Removing unneeded code

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-12 23:10:23 -03:00
Nafees NazikandHariom Balhara 7349fb9f6d test: add other fields to routing form e2e (#8112)
* chore: add test id

* chore: add test

* remove console log

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* fix: linting

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-04-12 13:58:03 -07:00
Syed Ali Shahbaz a8368aac79 Caldav/refactor: Mutiple Fixes and General code improvement (#8031)
* code cleanup and fixes for unsupported objects and fetch requests

* improves sanitizer function

* adds comment to the utility function sanitizeCalendarObjects

* adds ICAL.parse in try catch-- fixes code breaking for all objects due to one bad ICAL body

* adds a function that ensures expand=false fallback for unsupporting calendars

* --WIP

* improves code to now support multi-event calendar ics parsing

* Handle failed calls to not break all selected calendars

* removes debug remnants

* removes further unnecessary comments
2023-04-12 14:30:39 -03:00
Abdallah AlsammanandAlex van Andel 91f381bce9 Replace react icons with lucidedev (#8146)
* migrate from react-icons to lucide-react

* replace react-icon with lucide-dev: Webhook Icon

* add lucide transformer

* Fix LinkIcon import

* Update yarn.lock to include monorepo deps

* Migrated icons in ChargeCardDialog

* Port Storybook to new icons as well

* Adjust Info & Globe icons size to match react-icons size

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-12 17:26:31 +02:00
Peer Richelsen fb9c1b18e0 remove leading-none (#8232) 2023-04-12 12:15:08 +00:00
Alex van Andel 1d18a09cc8 v2.7.15
release.yml / release (push) Canceled after 0s
2023-04-12 13:58:42 +02:00
Peer Richelsen 887d63ceef app store fixes (#8224) 2023-04-12 11:40:53 +00:00
Hariom Balhara 555fcc21f2 Revert color-scheme for embed (#8229) 2023-04-12 11:16:59 +00:00
Peer Richelsen f45fb03838 wip: replaced dark border with border-subtle (#8211) 2023-04-12 10:42:41 +02:00
Peer Richelsen 46611fdf77 make routing form full width (#8215) 2023-04-12 07:39:04 +05:30
Crowdin Bot c0f7c40bec New Crowdin translations by Github Action 2023-04-12 01:57:27 +00:00
Joe Au-Yeungandzomars f6f257c705 Refactor app store & fix admin apps list bugs (#7812)
* If omni install do not show app successfully installed toast

* Clean up. Up to hubspot

* Fix double click to enable apps

* Clean up rest of apps

* Create cli app add dirName

* Type fix

* Only enable apps if keys are valid

* Save dirName as slug

* Remove dirname from metadata

---------

Co-authored-by: zomars <zomars@me.com>
2023-04-11 18:56:43 -07:00
Crowdin Bot d61f50724b New Crowdin translations by Github Action 2023-04-11 22:14:21 +00:00
Alex van Andel 960aa0e00b v2.7.14
release.yml / release (push) Canceled after 0s
2023-04-12 00:10:27 +02:00
Joe Au-YeungandAlex van Andel 817e20f11e Stripe add the ability to place hold on cards (#8022)
* Add payment option to schema

* Add payment option to Stripe zod

* Set payment option on event type

* Create manual payment intent in Stripe

* Set payment option from Stripe app

* Add payment option to DB

* Pass React.ReactNode to checkbox

* Create uncaptured payment intent

* WIP

* Capture card in setup intent

* Show charge card option

* Charge card from booking page

* Bug fixes

* Clean up

* Clean up app card

* Add no-show fee messaging on booking page

* Send payment email on payment & add price

* Fix messaging

* Create no show fee charged email

* Send charge fee collected email

* Disable submit on card failure

* Clean up

* Serverside prevent charging card again if already charged

* Only confirm booking if paid for

* Type fixes

* More type fixes

* More type fixes

* Type fix

* Type fixes

* UI changes

* Payment component rework

* Update apps/web/public/static/locales/en/common.json

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Update apps/web/public/static/locales/en/common.json

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Update apps/web/components/dialog/ChargeCardDialog.tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Update packages/trpc/server/routers/viewer/payments.tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Revert GTM config

* Adjust payment option dropdown

* Show alert when seats are set

* Small bug fixes

* Create collect card method

* clean up

* Prevent seats & charge no-show fee to be enabled together

* Do not charge no-show fee on unconfirmed bookings

* Add check to collect card method

* Webhook send request emails

* Fix some dark mode colours

* Change awaiting payment language

* Type fixes

* Set height of Select and TextField both to 38px to fix alignment

* Fix message seats & payment error message

* Type fix

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-11 23:44:14 +02:00
Keith Williams af0123a606 Updated issue/PR staleness checks (#8209)
* Updated issue/PR staleness checks

* Changed order of settings to group them better
2023-04-11 22:39:53 +02:00
zomars 709bbf6626 Update yarn.lock 2023-04-11 11:41:48 -07:00
zomars 53097ee3bc Delete lockfile.yml 2023-04-11 11:40:13 -07:00
zomars 31f2002654 Update lockfile.yml 2023-04-11 11:38:20 -07:00
zomars@users.noreply.github.com ee155c3643 Update yarn.lock 2023-04-11 18:23:33 +00:00
zomars 8ab20c3c9f Update lockfile.yml 2023-04-11 11:21:17 -07:00
zomars 4f6d035d29 Create lockfile.yml 2023-04-11 11:07:55 -07:00
zomars 2e5b42ef47 Update yarn.lock 2023-04-11 10:56:30 -07:00
Alex van Andel b055672ea0 v2.7.13
release.yml / release (push) Canceled after 0s
2023-04-11 19:51:38 +02:00
Alex van Andel e4fbca388c Fix yarn.lock file 2023-04-11 19:50:36 +02:00
Joe Au-Yeung 2b8bba09c9 Prevent updating app to enabled on seed app store (#8092) 2023-04-11 21:51:21 +05:30
Alex van Andel 98cf29f655 Wrap useSuspense with NoSSR (#8079) 2023-04-11 21:49:27 +05:30
GitStart-Cal.com ae8706316f CALCOM-7895 - [CAL-1320] remove "application_fee_amount" for all regions (#7933) 2023-04-11 16:16:43 +00:00
Jeroen ReumkensandAlex van Andel ec8e08d442 Added serverside translations to teams page. (#7999)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-11 21:39:02 +05:30
sean-brydonandPeer Richelsen c33f424333 Fixes /more page + fixes multiple duration badge on BookingPage (#8200)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-11 15:21:24 +00:00
Hariom Balhara 6f2312d1e2 Fix app not visible in app-store (#8202) 2023-04-11 14:58:57 +00:00
Peer Richelsen c2b6070b7e Update package.json
release.yml / release (push) Canceled after 0s
2023-04-11 16:54:49 +02:00
Efraín Rochín a5bebe2b48 bugfix/sync-not-working-properly (#8168) 2023-04-11 16:51:27 +02:00
sean-brydon e3a1f20cc8 Darkmode Fixes (#8191) 2023-04-11 12:15:10 +00:00
sean-brydon ab639591da Fix forcing hard refresh across forced themes (#8188) 2023-04-11 17:20:25 +05:30
Hariom BalharaandPeer Richelsen 21a9b64eb0 Fix missing sylaps icon in admin/apps (#8190)
* Fi missing sylaps icon in admin/apps and remove usage of imageSrc

* Remove imageSrc from googleCalendar and verify that logo is still shown

* Use logo in video apps location select:

* Remove imageSrc from templates

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-11 11:38:37 +00:00
Omar López 636ee7b8a1 Prioritizes published teams over unpublished ones (#8177) 2023-04-11 06:16:57 +00:00
Crowdin Bot 543e526568 New Crowdin translations by Github Action 2023-04-11 02:21:03 +00:00
Hariom Balhara e465c1d71d Redirect on top iframe always - This is to make sure embed redirects on top frame (#8164) 2023-04-11 07:50:15 +05:30
Peer Richelsen cdab9037ce added dark mode upgrade banner (#8172) 2023-04-10 21:14:28 +00:00
Crowdin Bot 21e454d0e2 New Crowdin translations by Github Action 2023-04-10 20:27:50 +00:00
zomars 7384f29a1c Update crowdin.yml 2023-04-10 13:27:00 -07:00
Keith Williams 9016a854ca v2.7.11
release.yml / release (push) Canceled after 0s
2023-04-10 16:46:55 -03:00
Nafees Nazik 446c29dd9c fix: booker phone input style (#8165)
* fix: text area styles

* fix: phone input styles

* fix: use the text area component

* fix: remove focus style
2023-04-10 18:12:15 +02:00
Leandro Miguel NarcisoandPeer Richelsen 2cf1f07d8f Fix loading indicator alignment touching the select input value (#8083)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-09 11:16:33 +00:00
Nafees NazikandPeer Richelsen 4f03f8e66d feat: dynamically elapse time left timer (#7938)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-09 10:20:51 +00:00
e1096620e6 fix: replace ltr and rtl with new ms and me (#8138)
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-09 11:13:57 +02:00
sean-brydon d416386d7b Fix loading state - force subtle token to exist (#8153)
* Fix loading state - force subtle token to exist

* Add opacity
2023-04-08 18:50:17 +02:00
Keith Williams 5b7c55b029 v2.7.10
release.yml / release (push) Canceled after 0s
2023-04-07 17:04:56 -03:00
sean-brydon 9b05311d7f Fixes team skeleton - force brand on onboarding (#8142) 2023-04-07 19:28:06 +00:00
sean-brydon 313d25d7d8 Force lightmode (#8141) 2023-04-07 19:17:22 +00:00
Keith Williams 5ec9c41f7c v2.7.9 2023-04-07 15:29:01 -03:00
Nafees Nazik d56386d016 fix: dark mode variant in team member card (#8137) 2023-04-07 15:52:21 +00:00
sydwardraeandJoe Au-Yeung 0968ede733 fixed open-source to open source (#8127)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-04-07 09:15:13 +02:00
alannnc 8e3ed28ba9 Switch order from user/slug (#8134) 2023-04-07 09:13:22 +02:00
Nafees NazikandAlex van Andel 82ea4e9fb7 fix: switch bg when default checked (#8129)
* fix: switch bg

* Update packages/ui/components/form/switch/Switch.tsx

Remove accidental space

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-06 20:37:05 +00:00
Nafees Nazik 8a440c24c0 fix: email line height (#8130) 2023-04-06 17:38:13 +00:00
Alex van Andel 88a8da3ef6 Update lockfile 2023-04-06 17:59:17 +02:00
Nafees Nazik 49290fba72 fix: flash of title in event types page (#8114) 2023-04-06 12:25:54 +02:00
Udit Takkar a363b1e903 fix: ui and ux improvements in booking questions (#7734) 2023-04-06 08:17:53 +00:00
Carina WollendorferandCarinaWolli 7b5e2c8bab Don't schedule cancelled workflow reminders (#8096)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-06 13:15:01 +05:30
sean-brydon 4b0d22655d Feat/tokens + darkmode + theme support (#7876) 2023-04-05 18:14:46 +00:00
Peer Richelsen 4f92f60d85 added illustrations to app store (#8104) 2023-04-05 19:05:44 +02:00
Nafees Nazik 6986b5af40 Fix: availability not deleteable (#8086)
* fix: make availability deletable

* test: add e2e

* chore: add test id

* test: add a new test

* fix: remove utility
2023-04-05 12:42:22 -03:00
Alex van Andel e043a5efd8 Async import of the appStore packages (#8087) 2023-04-05 11:55:57 -03:00
Peer Richelsen bff28e5d6f discord app (#8101) 2023-04-05 14:07:18 +02:00
GitStart-Cal.comandgitstart-calcom f10ff18d9f FIX-CALCOM-8009 (#8097)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-04-05 11:54:25 +02:00
Nafees Nazik cefe4754c7 fix: add older variables to whitelist (#8095) 2023-04-05 10:35:32 +02:00
703 changed files with 14657 additions and 7991 deletions
+2 -3
View File
@@ -160,9 +160,6 @@ CLOSECOM_API_KEY=
# Sendgrid internal sync service
SENDGRID_SYNC_API_KEY=
# Sentry
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_IGNORE_API_RESOLUTION_ERROR=
# Change your Brand
NEXT_PUBLIC_APP_NAME="Cal.com"
@@ -177,3 +174,5 @@ CSP_POLICY=
# Vercel Edge Config
EDGE_CONFIG=
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
+2 -1
View File
@@ -5,7 +5,8 @@
# the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
* @calcom/reviewers
# ⬇️ Removed so we can manually trigger checks
# * @calcom/reviewers
.github/ @calcom/core
.husky/ @calcom/core
+6 -3
View File
@@ -15,6 +15,9 @@ jobs:
steps:
- uses: actions/stale@v7
with:
days-before-stale: 60
include-only-assigned: true
days-before-close: -1
days-before-issue-stale: 60
days-before-issue-close: -1
days-before-pr-stale: 14
days-before-pr-close: 7
stale-pr-message: "This PR is being marked as stale due to inactivity."
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
+2
View File
@@ -15,6 +15,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
with:
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: crowdin action
uses: crowdin/github-action@1.5.1
+3 -2
View File
@@ -1,4 +1,5 @@
import React, { ReactElement, ReactNode } from "react";
import type { ReactElement, ReactNode } from "react";
import React from "react";
import { classNames } from "@calcom/lib";
@@ -42,7 +43,7 @@ export function VariantsTable({
</table>
</div>
{!isDark && (
<div data-mode="dark">
<div data-mode="dark" className="dark">
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
{children}
</VariantsTable>
@@ -193,3 +193,88 @@
.docs-story {
padding: 24px !important;
}
@layer {
:root {
/* background */
--cal-bg-emphasis: #E5E7EB;
--cal-bg: white;
--cal-bg-subtle: #F3F4F6;
--cal-bg-muted: #F9FAFB;
--cal-bg-inverted: #111827;
/* background -> components*/
--cal-bg-info: #DEE9FC;
--cal-bg-success: #E2FBE8;
--cal-bg-attention: #FCEED8;
--cal-bg-error: #F9E3E2;
/* Borders */
--cal-border-emphasis:#9CA3AF;
--cal-border: #D1D5DB;
--cal-border-subtle:#E5E7EB;
--cal-border-muted:#F3F4F6;
/* Content/Text */
--cal-text-emphasis: #111827;
--cal-text:#374151;
--cal-text-subtle:#6B7280;
--cal-text-muted:#9CA3AF;
--cal-text-inverted:white;
/* Content/Text -> components */
--cal-text-info:#253985;
--cal-text-success:#285231;
--cal-text-attention:#73321B;
--cal-text-error:#752522;
/* Brand shinanigans
-> These will be computed for the users theme at runtime.
*/
--cal-brand:#111827;
--cal-brand-emphasis:#101010;
}
.dark {
/* background */
--cal-bg-emphasis: #2B2B2B;
--cal-bg: #101010;
--cal-bg-subtle: #2B2B2B;
--cal-bg-muted: #1C1C1C;
--cal-bg-inverted: #F3F4F6;
/* background -> components*/
--cal-bg-info: #DEE9FC;
--cal-bg-success: #E2FBE8;
--cal-bg-attention: #FCEED8;
--cal-bg-error: #F9E3E2;
/* Borders */
--cal-border-emphasis: #575757;
--cal-border: #444444;
--cal-border-subtle: #2B2B2B;
--cal-border-muted: #1C1C1C;
/* Content/Text */
--cal-text-emphasis: #F3F4F6;
--cal-text: #D6D6D6;
--cal-text-subtle: #757575;
--cal-text-muted: #575757;
--cal-text-inverted: #101010;
/* Content/Text -> components */
--cal-text-info: #253985;
--cal-text-success: #285231;
--cal-text-attention: #73321B;
--cal-text-error: #752522;
/* Brand shenanigans
-> These will be computed for the users theme at runtime.
*/
--cal-brand: #111827;
--cal-brand-emphasis: #101010;
}
}
-2
View File
@@ -68,5 +68,3 @@ public/embed
# Copied app-store images
public/app-store
# Sentry
.sentryclirc
-53
View File
@@ -103,59 +103,6 @@ paths:
summary: Reset a user's password
tags:
- Authentication
"/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}":
get:
description: "Gets the busy times for a particular user, by username."
summary: Gets the busy times for a user
tags:
- Availability
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
description: ""
minItems: 1
uniqueItems: true
x-examples:
example-1:
- start: "Fri, 03 Sep 2021 17:00:00 GMT"
end: "Fri, 03 Sep 2021 17:40:00 GMT"
items:
type: object
properties:
start:
type: string
minLength: 1
end:
type: string
minLength: 1
required:
- start
- end
"500":
description: Internal Server Error
parameters:
- schema:
type: string
name: user
in: path
required: true
description: The username of who you want to check availability for
- schema:
type: string
name: dateFrom
in: path
required: true
description: The timestamp of which time you want to get busy times from
- schema:
type: string
name: dateTo
in: path
required: true
description: The timestamp of which time you want to get busy times until
/api/availability/calendar:
get:
description: Gets the user's selected calendars.
+3 -3
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { FiX } from "@calcom/ui/components/icon";
import { X } from "@calcom/ui/components/icon";
export default function AddToHomescreen() {
const { t } = useLocale();
@@ -29,7 +29,7 @@ export default function AddToHomescreen() {
<path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z" />
</svg>
</span>
<p className="text-xs font-medium text-white ltr:ml-3 rtl:mr-3">
<p className="text-inverted ms-3 text-xs font-medium">
<span className="inline">{t("add_to_homescreen")}</span>
</p>
</div>
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
type="button"
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">{t("dismiss")}</span>
<FiX className="h-6 w-6 text-white" aria-hidden="true" />
<X className="text-inverted h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
+4 -4
View File
@@ -6,7 +6,7 @@ import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Badge, ListItemText } from "@calcom/ui";
import { FiAlertCircle } from "@calcom/ui/components/icon";
import { AlertCircle } from "@calcom/ui/components/icon";
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
@@ -63,12 +63,12 @@ export default function AppListCard(props: AppListCardProps) {
}, []);
return (
<div className={`${highlight ? "bg-yellow-100" : ""}`}>
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
<div className="flex gap-x-3 px-5 py-4">
{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>
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
<div className="flex items-center gap-x-2">
{isDefault && <Badge variant="green">{t("default")}</Badge>}
{isTemplate && <Badge variant="red">Template</Badge>}
@@ -77,7 +77,7 @@ export default function AppListCard(props: AppListCardProps) {
<ListItemText component="p">{description}</ListItemText>
{invalidCredential && (
<div className="flex gap-x-2 pt-2">
<FiAlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")}
</ListItemText>
+39 -29
View File
@@ -22,7 +22,7 @@ import {
TextField,
ColorPicker,
} from "@calcom/ui";
import { FiCode, FiTrello, FiSun, FiArrowLeft } from "@calcom/ui/components/icon";
import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select";
@@ -493,7 +493,7 @@ const tabs = [
{
name: "HTML",
href: "embedTabName=embed-code",
icon: FiCode,
icon: Code,
type: "code",
Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -509,7 +509,7 @@ const tabs = [
return (
<>
<div>
<small className="flex py-4 text-gray-500">
<small className="text-subtle flex py-4">
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
</small>
</div>
@@ -517,7 +517,7 @@ const tabs = [
data-testid="embed-code"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-code"
className="h-[calc(100%-50px)] font-mono"
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
style={{ resize: "none", overflow: "auto" }}
readOnly
value={
@@ -534,7 +534,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
<!-- Cal ${embedType} embed code ends -->`
}
/>
<p className="hidden text-sm text-gray-500">
<p className="text-subtle hidden text-sm">
{t(
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
)}
@@ -546,7 +546,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
{
name: "React",
href: "embedTabName=embed-react",
icon: FiCode,
icon: Code,
type: "code",
Component: forwardRef<
HTMLTextAreaElement | HTMLIFrameElement | null,
@@ -561,12 +561,12 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
}
return (
<>
<small className="flex py-4 text-gray-500">{t("create_update_react_component")}</small>
<small className="text-subtle flex py-4">{t("create_update_react_component")}</small>
<TextArea
data-testid="embed-react"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-react"
className="h-[calc(100%-50px)] font-mono"
className="text-default bg-default selection:bg-subtle h-[calc(100%-50px)] font-mono"
readOnly
style={{ resize: "none", overflow: "auto" }}
value={`/* First make sure that you have installed the package */
@@ -586,7 +586,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
{
name: "Preview",
href: "embedTabName=embed-preview",
icon: FiTrello,
icon: Trello,
type: "iframe",
Component: forwardRef<
HTMLIFrameElement | HTMLTextAreaElement | null,
@@ -622,7 +622,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
return (
<components.Control {...props}>
<FiSun className="ml-2 h-4 w-4 text-gray-500" />
<Sun className="text-subtle ml-2 h-4 w-4" />
{children}
</components.Control>
);
@@ -634,17 +634,17 @@ const ChooseEmbedTypesDialogContent = () => {
return (
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
<div className="mb-2">
<h3 className="mb-2 text-2xl font-bold leading-none text-gray-900" id="modal-title">
<h3 className="font-cal text-emphasis mb-2 text-2xl font-bold leading-none" id="modal-title">
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
</h3>
<div>
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
<p className="text-subtle text-sm">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
</div>
</div>
<div className="flex items-start">
<div className="items-start space-y-2 md:flex md:space-y-0">
{embeds.map((embed, index) => (
<button
className="hover:border-primary-900 w-1/3 rounded-md border border-gray-200 bg-gray-50 p-6 text-left hover:rounded-md hover:bg-gray-100 ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0"
className="hover:bg-subtle bg-muted w-full rounded-md border border-transparent p-6 text-left hover:rounded-md ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0 lg:w-1/3"
key={index}
data-testid={embed.type}
onClick={() => {
@@ -652,11 +652,11 @@ const ChooseEmbedTypesDialogContent = () => {
embedType: embed.type,
});
}}>
<div className="order-none box-border flex-none rounded-md border border-solid bg-white">
<div className="bg-default order-none box-border flex-none rounded-md border border-solid dark:bg-transparent dark:invert">
{embed.illustration}
</div>
<div className="mt-4 font-semibold leading-none text-gray-900">{embed.title}</div>
<p className="mt-2 text-sm text-gray-500">{embed.subtitle}</p>
<div className="text-emphasis mt-4 font-semibold">{embed.title}</div>
<p className="text-subtle mt-2 text-sm">{embed.subtitle}</p>
</button>
))}
</div>
@@ -821,20 +821,20 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
enableOverflow
type="creation">
<div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
<div className="bg-muted flex w-1/3 flex-col p-8">
<h3
className="mb-2.5 flex items-center text-xl font-semibold leading-5 text-gray-900"
className="text-emphasis mb-2.5 flex items-center text-xl font-semibold leading-5"
id="modal-title">
<button
className="h-6 w-6"
onClick={() => {
removeQueryParams(router, ["embedType", "embedTabName"]);
}}>
<FiArrowLeft className="mr-4 w-4" />
<ArrowLeft className="mr-4 w-4" />
</button>
{embed.title}
</h3>
<h4 className="mb-6 text-sm font-normal text-gray-500">{embed.subtitle}</h4>
<h4 className="text-emphasis mb-6 text-sm font-normal">{embed.subtitle}</h4>
<div className="flex flex-col">
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
@@ -843,7 +843,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<CollapsibleContent className="text-sm">
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="mb-[9px] text-sm">Window sizing</div>
<div className="text-default mb-[9px] text-sm">Window sizing</div>
<div className="justify-left flex items-center !font-normal">
<div className="mr-[9px]">
<TextField
@@ -893,7 +893,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden"
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div className="mb-2 text-sm">Button Text</div>
{/* Default Values should come from preview iframe */}
@@ -917,7 +917,9 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div
className={classNames(
"mt-4 flex items-center justify-start",
embedType === "floating-popup" ? "space-x-2 rtl:space-x-reverse" : "hidden"
embedType === "floating-popup"
? "text-emphasis space-x-2 rtl:space-x-reverse"
: "hidden"
)}>
<Switch
defaultChecked={true}
@@ -933,12 +935,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
});
}}
/>
<div className="text-sm">Display Calendar Icon Button</div>
<div className="text-default text-sm">Display Calendar Icon Button</div>
</div>
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "" : "hidden"
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div className="mb-2">Position of Button</div>
<Select
@@ -957,7 +959,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={FloatingPopupPositionOptions}
/>
</div>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div
className={classNames(
"mt-4",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div>Button Color</div>
<div className="w-full">
<ColorPicker
@@ -978,7 +984,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/>
</div>
</div>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div
className={classNames(
"mt-4",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div>Text Color</div>
<div className="w-full">
<ColorPicker
@@ -1020,7 +1030,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
});
}}
/>
<div className="text-sm">{t("hide_eventtype_details")}</div>
<div className="text-default text-sm">{t("hide_eventtype_details")}</div>
</div>
{[
{ name: "brandColor", title: "Brand Color" },
+4 -4
View File
@@ -123,7 +123,7 @@ export default function ImageUploader({
<DialogContent>
<div className="mb-4 sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<h3 className="font-cal text-lg leading-6 text-gray-900" id="modal-title">
<h3 className="font-cal text-emphasis text-lg leading-6" id="modal-title">
{t("upload_target", { target })}
</h3>
</div>
@@ -131,9 +131,9 @@ export default function ImageUploader({
<div className="mb-4">
<div className="cropper mt-6 flex flex-col items-center justify-center p-8">
{!result && (
<div className="flex h-20 max-h-20 w-20 items-center justify-start rounded-full bg-gray-50">
<div className="bg-muted flex h-20 max-h-20 w-20 items-center justify-start rounded-full">
{!imageSrc && (
<p className="w-full text-center text-sm text-white sm:text-xs">
<p className="text-inverted w-full text-center text-sm sm:text-xs">
{t("no_target", { target })}
</p>
)}
@@ -144,7 +144,7 @@ export default function ImageUploader({
</div>
)}
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 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">
<label className="border-default bg-default hover:bg-mutedover:text-emphasis dark:hover:bg-inverted text-default dark:text-inverted focus:ring-empthasis mt-8 rounded-sm border px-3 py-1 text-xs font-medium leading-4 focus:outline-none focus:ring-2 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent">
<input
onInput={onInputFile}
type="file"
+4 -4
View File
@@ -69,8 +69,8 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
onClick={onClick}
className={classNames(
isCurrent
? "border-gray-900 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
? "text-emphasis border-gray-900"
: "hover:border-default hover:text-default text-subtle border-transparent",
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
className
)}
@@ -78,7 +78,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
{tab.icon && (
<tab.icon
className={classNames(
isCurrent ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
isCurrent ? "text-emphasis" : "group-hover:text-subtle text-muted",
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
)}
aria-hidden="true"
@@ -91,7 +91,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
);
})}
</nav>
<hr />
<hr className="border-subtle" />
</>
);
};
+99
View File
@@ -0,0 +1,99 @@
import { DefaultSeo } from "next-seo";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
import Script from "next/script";
import "@calcom/embed-core/src/embed-iframe";
import { useEmbedUiConfig } from "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import type { AppProps } from "@lib/app-providers";
import AppProviders from "@lib/app-providers";
import { seoConfig } from "@lib/config/next-seo.config";
import I18nLanguageHandler from "@components/I18nLanguageHandler";
export interface CalPageWrapper {
(props?: any): JSX.Element;
PageWrapper?: AppProps["Component"]["PageWrapper"];
}
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
const calFont = localFont({
src: "../fonts/CalSans-SemiBold.woff2",
variable: "--font-cal",
preload: true,
display: "swap",
});
function PageWrapper(props: AppProps) {
const { Component, pageProps, err, router } = props;
let pageStatus = "200";
const { cssVarsPerTheme } = useEmbedUiConfig();
const cssVarsStyle = [];
if (cssVarsPerTheme) {
for (const [themeName, cssVars] of Object.entries(cssVarsPerTheme)) {
cssVarsStyle.push(`.${themeName} {`);
for (const [cssVarName, value] of Object.entries(cssVars)) {
cssVarsStyle.push(`--${cssVarName}: ${value};`);
}
cssVarsStyle.push(`}`);
}
}
if (router.pathname === "/404") {
pageStatus = "404";
} else if (router.pathname === "/500") {
pageStatus = "500";
}
// On client side don't let nonce creep into DOM
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
// See https://github.com/kentcdodds/nonce-hydration-issues
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
const providerProps = {
...props,
pageProps: {
...props.pageProps,
nonce,
},
};
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
return (
<AppProviders {...providerProps}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<I18nLanguageHandler />
<Script
nonce={nonce}
id="page-status"
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
/>
<style jsx global>{`
:root {
--font-inter: ${interFont.style.fontFamily};
--font-cal: ${calFont.style.fontFamily};
}
`}</style>
<style jsx global>
{`
${cssVarsStyle.join("")}
`}
</style>
{getLayout(
Component.requiresLicense ? (
<LicenseRequired>
<Component {...pageProps} err={err} />
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
),
router
)}
</AppProviders>
);
}
export default PageWrapper;
+7 -7
View File
@@ -3,7 +3,7 @@ import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary } from "@calcom/ui";
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
import { CreditCard, Key, Lock, Terminal, User, Users } from "@calcom/ui/components/icon";
import NavTabs from "./NavTabs";
@@ -11,32 +11,32 @@ const tabs = [
{
name: "profile",
href: "/settings/my-account/profile",
icon: FiUser,
icon: User,
},
{
name: "teams",
href: "/settings/teams",
icon: FiUsers,
icon: Users,
},
{
name: "security",
href: "/settings/security",
icon: FiKey,
icon: Key,
},
{
name: "developer",
href: "/settings/developer",
icon: FiTerminal,
icon: Terminal,
},
{
name: "billing",
href: "/settings/billing",
icon: FiCreditCard,
icon: CreditCard,
},
{
name: "admin",
href: "/settings/admin",
icon: FiLock,
icon: Lock,
adminRequired: true,
},
];
@@ -9,7 +9,7 @@ import {
DropdownMenuItem,
DropdownItem,
} from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import { Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -40,7 +40,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
return (
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
{t("add")}
</Button>
</DropdownMenuTrigger>
@@ -48,7 +48,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
{options.map((data) => (
<DropdownMenuItem key={data.slug} className="focus:outline-none">
{data.slug === "add-new" ? (
<DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
{t("install_new_calendar_app")}
</DropdownItem>
) : (
+31 -34
View File
@@ -14,16 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App as AppType } from "@calcom/types/App";
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
import {
FiBookOpen,
FiCheck,
FiExternalLink,
FiFile,
FiFlag,
FiMail,
FiPlus,
FiShield,
} from "@calcom/ui/components/icon";
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
/* These app slugs all require Google Cal to be installed */
@@ -96,7 +87,7 @@ const Component = ({
return (
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
{hasDescriptionItems && (
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
<div className="align-center bg-subtle mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
{descriptionItems ? (
descriptionItems.map((descriptionItem, index) =>
typeof descriptionItem === "object" ? (
@@ -127,13 +118,17 @@ const Component = ({
<div className="mb-8 flex pt-4">
<header>
<div className="mb-4 flex items-center">
<img className="min-h-16 min-w-16 h-16 w-16" src={logo} alt={name} />
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
<img
className={classNames(logo.includes("-dark") && "dark:invert", "min-h-16 min-w-16 h-16 w-16")}
src={logo}
alt={name}
/>
<h1 className="font-cal text-emphasis ml-4 text-3xl">{name}</h1>
</div>
<h2 className="text-sm font-medium text-gray-600">
<h2 className="text-default text-sm font-medium">
<Link
href={`categories/${categories[0]}`}
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
{categories[0]}
</Link>{" "}
• {t("published_by", { author })}
@@ -149,7 +144,7 @@ const Component = ({
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
<Button StartIcon={FiCheck} color="secondary" disabled>
<Button StartIcon={Check} color="secondary" disabled>
{existingCredentials.length > 0
? t("active_install", { count: existingCredentials.length })
: t("default")}
@@ -171,7 +166,7 @@ const Component = ({
}
return (
<Button
StartIcon={FiPlus}
StartIcon={Plus}
{...props}
// @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now.
@@ -243,9 +238,11 @@ const Component = ({
</span>
)}
<div className="prose-sm prose mt-8">{body}</div>
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
<span>
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
{body}
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{price === 0 ? (
t("free_to_use_apps")
) : (
@@ -260,16 +257,16 @@ const Component = ({
)}
</span>
<h4 className="mt-8 mb-2 font-semibold text-gray-900 ">{t("learn_more")}</h4>
<h4 className="text-emphasis mt-8 mb-2 font-semibold ">{t("learn_more")}</h4>
<ul className="prose-sm -ml-1 -mr-1 leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-sm font-normal text-black no-underline hover:underline"
className="text-emphasis text-sm font-normal no-underline hover:underline"
href={docs}>
<FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
<BookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
@@ -279,9 +276,9 @@ const Component = ({
<a
target="_blank"
rel="noreferrer"
className="font-normal text-black no-underline hover:underline"
className="text-emphasis font-normal no-underline hover:underline"
href={website}>
<FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
<ExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
@@ -291,9 +288,9 @@ const Component = ({
<a
target="_blank"
rel="noreferrer"
className="font-normal text-black no-underline hover:underline"
className="text-emphasis font-normal no-underline hover:underline"
href={"mailto:" + email}>
<FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
<Mail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{email}
</a>
@@ -304,9 +301,9 @@ const Component = ({
<a
target="_blank"
rel="noreferrer"
className="font-normal text-black no-underline hover:underline"
className="text-emphasis font-normal no-underline hover:underline"
href={tos}>
<FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
<File className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
@@ -316,20 +313,20 @@ const Component = ({
<a
target="_blank"
rel="noreferrer"
className="font-normal text-black no-underline hover:underline"
className="text-emphasis font-normal no-underline hover:underline"
href={privacy}>
<FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
<Shield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="my-8" />
<span className="leading-1 block text-xs text-gray-500">
<hr className="border-subtle my-8 border" />
<span className="leading-1 text-subtle block text-xs">
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
<Flag className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
</div>
@@ -17,7 +17,7 @@ import {
Switch,
ShellSubHeading,
} from "@calcom/ui";
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
import { ArrowLeft, Calendar, Plus } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -99,8 +99,8 @@ function CalendarSwitch(props: {
}}
/>
{!!props.destination && (
<span className="ml-8 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800 sm:ml-4">
<FiArrowLeft className="h-4 w-4" />
<span className="bg-subtle text-default ml-8 inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm font-normal sm:ml-4">
<ArrowLeft className="h-4 w-4" />
{t("adding_events_to")}
</span>
)}
@@ -177,14 +177,14 @@ function ConnectedCalendarsList(props: Props) {
credentialId={item.credentialId}
trashIcon
onSuccess={props.onChanged}
buttonProps={{ className: "border border-gray-300" }}
buttonProps={{ className: "border border-default" }}
/>
</div>
}>
<div className="border-t border-gray-200">
<div className="border-subtle border-t">
{!fromOnboarding && (
<>
<p className="px-5 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
<ul className="space-y-2 px-5 py-4">
{item.calendars.map((cal) => (
<CalendarSwitch
@@ -218,7 +218,7 @@ function ConnectedCalendarsList(props: Props) {
credentialId={item.credentialId}
trashIcon
onSuccess={props.onChanged}
buttonProps={{ className: "border border-gray-300" }}
buttonProps={{ className: "border border-default" }}
/>
</div>
}
@@ -264,7 +264,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
<>
{heading && (
<div className="flex flex-col gap-6 rounded-md border p-7">
<div className="border-default flex flex-col gap-6 rounded-md border p-7">
<ShellSubHeading
title={t("calendar")}
subtitle={t("installed_app_calendar_description")}
@@ -279,18 +279,18 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</div>
}
/>
<div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4">
<div className="bg-muted border-subtle flex justify-between rounded-md border p-4">
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
<div className="relative rounded-md border border-gray-200 bg-white p-1.5">
<FiCalendar className="h-8 w-8" strokeWidth="1" />
<FiPlus
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black"
<div className="bg-default border-subtle relative rounded-md border p-1.5">
<Calendar className="text-default h-8 w-8" strokeWidth="1" />
<Plus
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
strokeWidth="4"
/>
</div>
<div className="md:w-6/12">
<h1 className="text-sm font-semibold">{t("create_events_on")}</h1>
<p className="text-sm font-normal">{t("set_calendar")}</p>
<h1 className="text-emphasis text-sm font-semibold">{t("create_events_on")}</h1>
<p className="text-default text-sm font-normal">{t("set_calendar")}</p>
</div>
<div className="justify-end md:w-6/12">
<DestinationCalendarSelector
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</>
) : (
<EmptyScreen
Icon={FiCalendar}
Icon={Calendar}
headline={t("no_category_apps", {
category: t("calendar").toLowerCase(),
})}
@@ -6,7 +6,7 @@ import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui";
import { FiAlertCircle } from "@calcom/ui/components/icon";
import { AlertCircle } from "@calcom/ui/components/icon";
type AppsLayoutProps = {
children: React.ReactNode;
@@ -27,7 +27,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
<main className="w-full">
{emptyStore ? (
<EmptyScreen
Icon={FiAlertCircle}
Icon={AlertCircle}
headline={t("no_apps")}
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
@@ -6,38 +6,38 @@ import type { InstalledAppVariants } from "@calcom/app-store/utils";
import Shell from "@calcom/features/shell/Shell";
import { trpc } from "@calcom/trpc/react";
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
import { FiBarChart, FiCalendar, FiCreditCard, FiGrid, FiShare2, FiVideo } from "@calcom/ui/components/icon";
import { BarChart, Calendar, CreditCard, Grid, Share2, Video } from "@calcom/ui/components/icon";
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
name: "calendar",
href: "/apps/installed/calendar",
icon: FiCalendar,
icon: Calendar,
},
{
name: "conferencing",
href: "/apps/installed/conferencing",
icon: FiVideo,
icon: Video,
},
{
name: "payment",
href: "/apps/installed/payment",
icon: FiCreditCard,
icon: CreditCard,
},
{
name: "automation",
href: "/apps/installed/automation",
icon: FiShare2,
icon: Share2,
},
{
name: "analytics",
href: "/apps/installed/analytics",
icon: FiBarChart,
icon: BarChart,
},
{
name: "other",
href: "/apps/installed/other",
icon: FiGrid,
icon: Grid,
},
];
+1 -1
View File
@@ -28,7 +28,7 @@ export default function TwoFactor({ center = true }) {
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
<Label className="mt-4"> {t("2fa_code")}</Label>
<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
<div className="flex flex-row justify-between">
<Input
@@ -26,7 +26,7 @@ export default function AdminLayout({
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
return (
<SettingsLayout {...rest}>
<div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200">
<div className="divide-subtle mx-auto flex max-w-4xl flex-row divide-y">
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
<ErrorBoundary>{children}</ErrorBoundary>
</div>
@@ -1,13 +1,13 @@
import React from "react";
import { Button, SkeletonText } from "@calcom/ui";
import { FiMoreHorizontal } from "@calcom/ui/components/icon";
import { MoreHorizontal } from "@calcom/ui/components/icon";
import classNames from "@lib/classNames";
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<ul className="divide-subtle border-subtle bg-default animate-pulse divide-y rounded-md border sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
@@ -20,8 +20,8 @@ export default SkeletonLoader;
function SkeletonItem() {
return (
<li>
<div className="flex items-center justify-between py-5 hover:bg-neutral-50 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
<div className="items-between flex w-full flex-col justify-center hover:bg-neutral-50 sm:px-6">
<div className="flex items-center justify-between py-5 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
<div className="items-between flex w-full flex-col justify-center sm:px-6">
<SkeletonText className="my-1 h-4 w-32" />
<SkeletonText className="my-1 h-2 w-24" />
<SkeletonText className="my-1 h-2 w-40" />
@@ -31,7 +31,7 @@ function SkeletonItem() {
type="button"
variant="icon"
color="secondary"
StartIcon={FiMoreHorizontal}
StartIcon={MoreHorizontal}
disabled
/>
</div>
@@ -43,7 +43,7 @@ export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
return (
<li
className={classNames(
"group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3",
"border-subtle group flex w-full items-center justify-between rounded-sm border px-[10px] py-3",
className
)}>
<div className="flex-grow truncate text-sm">
@@ -4,7 +4,7 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "@calcom/ui";
import { FiLink } from "@calcom/ui/components/icon";
import { Link } from "@calcom/ui/components/icon";
import type { Props } from "./pages/AvailabilityPage";
@@ -41,7 +41,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
<Link className="text-default min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] " />
) : (
<img
src={eventLocationType.iconUrl}
@@ -61,7 +61,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
return filteredLocations.length ? (
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
<div className="text-default mr-6 flex w-full flex-col space-y-4 break-words text-sm">
{filteredLocations}
</div>
) : null;
+19 -7
View File
@@ -10,6 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { nameOfDay } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
@@ -28,6 +29,7 @@ type AvailableTimesProps = {
slots?: Slot[];
isLoading: boolean;
ethSignature?: string;
duration: number;
};
const AvailableTimes: FC<AvailableTimesProps> = ({
@@ -42,7 +44,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
seatsPerTimeSlot,
bookingAttendees,
ethSignature,
duration,
}) => {
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
const { t, i18n } = useLocale();
const router = useRouter();
@@ -63,16 +67,24 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
[isMobile]
);
const reserveSlot = (slot: Slot) => {
reserveSlotMutation.mutate({
slotUtcStartDate: slot.time,
eventTypeId,
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
});
};
return (
<div ref={slotPickerRef}>
{!!date ? (
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
<div className="mt-8 flex h-full w-full flex-col rounded-md px-4 text-center sm:mt-0 sm:p-4 md:-mb-5 md:min-w-[200px] md:p-4 lg:min-w-[300px]">
<div className="mb-4 flex items-center text-left text-base">
<div className="mr-4">
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
<span className="text-emphasis font-semibold">
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-bookinglight">
<span className="text-subtle">
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span>
</div>
@@ -132,7 +144,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{seatsPerTimeSlot && slot.attendees && (slotFull || notEnoughSeats) ? (
<div
className={classNames(
"text-primary-500 dark:bg-darkgray-200 dark:text-darkgray-900 mb-2 block rounded-sm border bg-white py-2 font-medium opacity-25 dark:border-transparent ",
"text-default bg-default border-subtle mb-2 block rounded-sm border py-2 font-medium opacity-25",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}>
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
@@ -147,10 +159,10 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
href={bookingUrl}
prefetch={false}
className={classNames(
"hover:bg-brand hover:border-brand hover:text-brandcontrast dark:hover:text-darkmodebrandcontrast",
"dark:bg-darkgray-200 dark:hover:bg-darkmodebrand 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",
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
brand === "#fff" || brand === "#ffffff" ? "" : ""
)}
onClick={() => reserveSlot(slot)}
data-testid="time">
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && (
@@ -174,7 +186,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{!isLoading && !slots.length && (
<div className="-mt-4 flex h-full w-full flex-col content-center items-center justify-center">
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
<h1 className="text-emphasis my-6 text-xl">{t("all_booked_today")}</h1>
</div>
)}
@@ -6,7 +6,7 @@ import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge } from "@calcom/ui";
import { FiCheckSquare, FiClock } from "@calcom/ui/components/icon";
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -83,32 +83,27 @@ const BookingDescription: FC<Props> = (props) => {
size="sm"
truncateAfter={3}
/>
<h2 className="mt-1 mb-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
{profile.name}
</h2>
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold leading-none text-gray-900">
<h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">{profile.name}</h2>
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
{eventType.title}
</h1>
<div className="dark:text-darkgray-600 flex flex-col space-y-4 text-sm font-medium text-gray-600">
<div className=" text-default flex flex-col space-y-4 text-sm font-medium">
{eventType?.description && (
<div
className={classNames(
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
isBookingPage && "text-default text-sm font-medium"
)}>
{/* TODO: Fix colors when token is introdcued to DS */}
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
<EventTypeDescriptionSafeHTML eventType={eventType} />
</div>
</div>
)}
{requiresConfirmation && (
<div
className={classNames(
"items-top flex",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}>
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
<div>
<FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<CheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
</div>
{requiresConfirmationText}
</div>
@@ -119,12 +114,12 @@ const BookingDescription: FC<Props> = (props) => {
<div
className={classNames(
"flex flex-nowrap text-sm font-medium",
isBookingPage && "dark:text-darkgray-600 text-gray-600",
isBookingPage && "text-default",
!eventType.metadata?.multipleDuration && "items-center"
)}>
<FiClock
<Clock
className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
isBookingPage && "mt-[2px]"
)}
/>
@@ -137,9 +132,9 @@ const BookingDescription: FC<Props> = (props) => {
variant="gray"
className={classNames(
duration === dur.toString()
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-gray-200 text-gray-900 dark:text-white",
"cursor-pointer"
? "bg-emphasis border-emphasis text-emphasis "
: "bg-subtle text-default border-transparent ",
"cursor-pointer border"
)}
onClick={() => {
setDuration(dur);
@@ -1,22 +1,35 @@
import type { TFunction } from "next-i18next";
import { FormattedNumber, IntlProvider } from "react-intl";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { FiCreditCard } from "@calcom/ui/components/icon";
import { CreditCard } from "@calcom/ui/components/icon";
const BookingDescriptionPayment = (props: { eventType: Parameters<typeof getPaymentAppData>[0] }) => {
const BookingDescriptionPayment = (props: {
eventType: Parameters<typeof getPaymentAppData>[0];
t: TFunction;
}) => {
const paymentAppData = getPaymentAppData(props.eventType);
if (!paymentAppData || paymentAppData.price <= 0) return null;
return (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>
{props.t("no_show_fee_amount", {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
})}
</>
) : (
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
)}
</p>
);
};
+73 -36
View File
@@ -28,10 +28,11 @@ import {
TableActions,
TextAreaField,
} from "@calcom/ui";
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } from "@calcom/ui/components/icon";
import useMeQuery from "@lib/hooks/useMeQuery";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
@@ -56,7 +57,9 @@ function BookingListItem(booking: BookingItemProps) {
const router = useRouter();
const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const cardCharged = booking?.payment[0]?.success;
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data?.status === BookingStatus.REJECTED) {
@@ -113,19 +116,23 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setRejectionDialogIsOpen(true);
},
icon: FiSlash,
icon: Slash,
disabled: mutation.isLoading,
},
{
id: "confirm",
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
onClick: () => {
bookingConfirm(true);
},
icon: FiCheck,
disabled: mutation.isLoading,
color: "primary",
},
// For bookings with payment, only confirm if the booking is paid for
...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
? [
{
id: "confirm",
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
onClick: () => {
bookingConfirm(true);
},
icon: Check,
disabled: mutation.isLoading,
},
]
: []),
];
const showRecordingActions: ActionType[] = [
@@ -149,7 +156,7 @@ function BookingListItem(booking: BookingItemProps) {
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
`,
icon: FiX,
icon: X,
},
{
id: "edit_booking",
@@ -157,7 +164,7 @@ function BookingListItem(booking: BookingItemProps) {
actions: [
{
id: "reschedule",
icon: FiClock,
icon: Clock,
label: t("reschedule_booking"),
href: `/reschedule/${booking.uid}${
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
@@ -165,7 +172,7 @@ function BookingListItem(booking: BookingItemProps) {
},
{
id: "reschedule_request",
icon: FiSend,
icon: Send,
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
label: t("send_reschedule_request"),
onClick: () => {
@@ -178,12 +185,24 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setIsOpenLocationDialog(true);
},
icon: FiMapPin,
icon: MapPin,
},
],
},
];
const chargeCardActions: ActionType[] = [
{
id: "charge_card",
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
disabled: cardCharged,
onClick: () => {
setChargeCardDialogIsOpen(true);
},
icon: CreditCard,
},
];
if (isTabRecurring && isRecurring) {
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
}
@@ -194,7 +213,7 @@ function BookingListItem(booking: BookingItemProps) {
const RequestSentMessage = () => {
return (
<Badge startIcon={FiSend} size="md" variant="gray" data-testid="request_reschedule_sent">
<Badge startIcon={Send} size="md" variant="gray" data-testid="request_reschedule_sent">
{t("reschedule_request_sent")}
</Badge>
);
@@ -237,10 +256,14 @@ function BookingListItem(booking: BookingItemProps) {
},
});
};
const title = booking.title;
// To be used after we run query on legacy bookings
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
const showRecordingsButtons =
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
const title = booking.title;
return (
<>
<RescheduleDialog
@@ -254,6 +277,15 @@ function BookingListItem(booking: BookingItemProps) {
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
/>
{booking.paid && (
<ChargeCardDialog
isOpenDialog={chargeCardDialogIsOpen}
setIsOpenDialog={setChargeCardDialogIsOpen}
bookingId={booking.id}
paymentAmount={booking?.payment[0].amount}
paymentCurrency={booking?.payment[0].currency}
/>
)}
{showRecordingsButtons && (
<ViewRecordingsDialog
booking={booking}
@@ -271,7 +303,7 @@ function BookingListItem(booking: BookingItemProps) {
label={
<>
{t("rejection_reason")}
<span className="font-normal text-gray-500"> (Optional)</span>
<span className="text-subtle font-normal"> (Optional)</span>
</>
}
value={rejectionReason}
@@ -292,13 +324,13 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</Dialog>
<tr className="group flex flex-col hover:bg-gray-50 sm:flex-row">
<tr className="hover:bg-muted group flex flex-col sm:flex-row">
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
<div className="cursor-pointer py-4">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
<div className="text-emphasis text-sm leading-6">{startTime}</div>
<div className="text-subtle text-sm">
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones
@@ -321,11 +353,11 @@ function BookingListItem(booking: BookingItemProps) {
)}
{booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
{t("paid")}
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
</Badge>
)}
{recurringDates !== undefined && (
<div className="mt-2 text-sm text-gray-400">
<div className="text-muted mt-2 text-sm">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div>
)}
@@ -335,8 +367,8 @@ function BookingListItem(booking: BookingItemProps) {
{/* Time and Badges for mobile */}
<div className="w-full pt-4 pb-2 sm:hidden">
<div className="flex w-full items-center justify-between sm:hidden">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="pr-2 text-sm text-gray-500">
<div className="text-emphasis text-sm leading-6">{startTime}</div>
<div className="text-subtle pr-2 text-sm">
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
<MeetingTimeInTimezones
@@ -365,7 +397,7 @@ function BookingListItem(booking: BookingItemProps) {
</Badge>
)}
{recurringDates !== undefined && (
<div className="text-sm text-gray-400 sm:hidden">
<div className="text-muted text-sm sm:hidden">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
</div>
)}
@@ -375,21 +407,21 @@ function BookingListItem(booking: BookingItemProps) {
<div
title={title}
className={classNames(
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
"max-w-10/12 sm:max-w-56 text-emphasis text-sm font-medium leading-6 md:max-w-full",
isCancelled ? "line-through" : ""
)}>
{title}
<span> </span>
{!!booking?.eventType?.price && !booking.paid && (
<Badge className="hidden ltr:ml-2 ltr:mr-2 rtl:ml-2 sm:inline-flex" variant="orange">
<Badge className="ms-2 me-2 hidden sm:inline-flex" variant="orange">
{t("pending_payment")}
</Badge>
)}
</div>
{booking.description && (
<div
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 text-default truncate text-sm"
title={booking.description}>
&quot;{booking.description}&quot;
</div>
@@ -413,7 +445,7 @@ function BookingListItem(booking: BookingItemProps) {
<>
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
{isConfirmed && <TableActions actions={bookedActions} />}
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
{isRejected && <div className="text-subtle text-sm">{t("rejected")}</div>}
</>
) : null}
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
@@ -423,6 +455,11 @@ function BookingListItem(booking: BookingItemProps) {
<RequestSentMessage />
</div>
)}
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
<div className="ml-2">
<TableActions actions={chargeCardActions} />
</div>
)}
</td>
</tr>
</>
@@ -475,10 +512,10 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
</p>
);
})}>
<div className="text-gray-600 dark:text-white">
<FiRefreshCcw
<div className="text-default">
<RefreshCcw
strokeWidth="3"
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
/>
<p className="mt-1 pl-5 text-xs">
{booking.status === BookingStatus.ACCEPTED
@@ -551,13 +588,13 @@ const DisplayAttendees = ({
}) => {
const { t } = useLocale();
return (
<div className="text-sm text-gray-900">
<div className="text-emphasis text-sm">
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
{attendees.length > 1 ? <span>,&nbsp;</span> : <span>&nbsp;{t("and")}&nbsp;</span>}
<Attendee {...attendees[0]} />
{attendees.length > 1 && (
<>
<div className="inline-block text-sm text-gray-900">&nbsp;{t("and")}&nbsp;</div>
<div className="text-emphasis inline-block text-sm">&nbsp;{t("and")}&nbsp;</div>
{attendees.length > 2 ? (
<Tooltip
content={attendees.slice(1).map((attendee) => (
@@ -2,11 +2,10 @@ import { useRouter } from "next/router";
import { useCallback, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, TextArea } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
import { X } from "@calcom/ui/components/icon";
type Props = {
booking: {
@@ -34,7 +33,6 @@ export default function CancelBooking(props: Props) {
const [loading, setLoading] = useState(false);
const telemetry = useTelemetry();
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
useTheme(props.theme);
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
if (node !== null) {
@@ -47,11 +45,11 @@ export default function CancelBooking(props: Props) {
<>
{error && (
<div className="mt-8">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<FiX className="h-6 w-6 text-red-600" />
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<X className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
{error}
</h3>
</div>
@@ -59,13 +57,13 @@ export default function CancelBooking(props: Props) {
)}
{!error && (
<div className="mt-5 sm:mt-6">
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
<label className="text-default font-medium">{t("cancellation_reason")}</label>
<TextArea
ref={cancelBookingRef}
placeholder={t("cancellation_reason_placeholder")}
value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)}
className="dark:bg-darkgray-100 dark:border-darkgray-400 mt-2 mb-4 w-full dark:text-white "
className="mt-2 mb-4 w-full "
rows={3}
/>
<div className="flex flex-col-reverse rtl:space-x-reverse ">
@@ -77,7 +75,6 @@ export default function CancelBooking(props: Props) {
{t("nevermind")}
</Button>
<Button
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast flex justify-center"
data-testid="cancel"
onClick={async () => {
setLoading(true);
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
<ul className="divide-subtle border-subtle bg-default animate-pulse divide-y rounded-md border sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
+2 -1
View File
@@ -164,7 +164,7 @@ export const SlotPicker = ({
isLoading={isLoading}
className={classNames(
"mt-8 px-4 pb-4 sm:mt-0 md:min-w-[300px] md:px-4 lg:min-w-[455px]",
selectedDate ? "sm:dark:border-darkgray-200 border-gray-200 sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
selectedDate ? " border-subtle sm:border-r sm:p-4 sm:pr-6" : "sm:p-4"
)}
includedDates={Object.keys(monthSlots).filter((k) => monthSlots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
@@ -194,6 +194,7 @@ export const SlotPicker = ({
bookingAttendees={bookingAttendees}
recurringCount={recurringEventCount}
ethSignature={ethSignature}
duration={parseInt(duration)}
/>
</>
);
+2 -1
View File
@@ -27,7 +27,8 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
<TimezoneSelect
id="timeZone"
classNames={{
singleValue: () => "dark:text-darkgray-600 text-gray-600",
singleValue: () => "text-default",
dropdownIndicator: () => "text-default",
menu: () => "!w-64 max-w-[90vw]",
}}
variant="minimal"
@@ -1,4 +1,4 @@
import { FiGlobe } from "@calcom/ui/components/icon";
import { Globe } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
@@ -18,7 +18,7 @@ export function TimezoneDropdown({
return (
<>
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
</div>
</>
@@ -14,18 +14,17 @@ import {
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { HeadSeo } from "@calcom/ui";
import { FiCreditCard, FiUser, FiRefreshCcw } from "@calcom/ui/components/icon";
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
@@ -58,12 +57,24 @@ const dateQuerySchema = z.object({
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor: string; darkBrandColor: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const router = useRouter();
const isEmbed = useIsEmbed(restProps.isEmbed);
const query = dateQuerySchema.parse(router.query);
const { rescheduleUid } = query;
useTheme(profile.theme);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const { t } = useLocale();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
@@ -93,8 +104,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
/*
const telemetry = useTelemetry();
useEffect(() => {
useEffect(() => {
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
@@ -102,7 +114,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
);
}
}, [telemetry]);
}, [telemetry]); */
const embedUiConfig = useEmbedUiConfig();
// get dynamic user list here
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
@@ -112,6 +124,16 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
[timeZone]
);
const paymentAppData = getPaymentAppData(eventType);
const paymentAmount = () => {
return;
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>;
};
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
@@ -157,30 +179,27 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
isBrandingHidden={restProps.isBrandingHidden}
/>
<BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div>
<main
className={classNames(
"flex flex-col md:mx-4",
shouldAlignCentrally ? "items-center" : "items-start",
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
!isEmbed && classNames("bg-subtle dark:bg-default mx-auto my-0 ease-in-out md:my-24")
)}>
<div>
<div
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent
? ""
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
"border-bookinglightest md:rounded-md md:border",
isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
"border-booker md:border-booker-width md:rounded-md",
isEmbed && "mx-auto"
)}>
<div className="md:flex">
{showEventTypeDetails && (
<div
className={classNames(
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
"min-w-full md:w-[280px] md:min-w-[280px]",
" border-subtle flex flex-col p-5 sm:border-r",
"min-w-full md:w-[230px] md:min-w-[230px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
@@ -188,10 +207,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
<div
className={classNames(
"flex flex-nowrap items-center text-sm font-medium",
"dark:text-darkgray-600 text-gray-600",
" text-default",
"ltr:mr-[10px] rtl:ml-[10px]"
)}>
<FiUser
<User
className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"mt-[2px]"
@@ -202,21 +221,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
)}
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<RefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<div>
<p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<input
type="number"
<NumberInput
defaultValue={eventType.recurringEvent.count}
min="1"
max={eventType.recurringEvent.count}
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
defaultValue={eventType.recurringEvent.count}
isFullWidth={false}
className="me-2 inline w-16"
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
<p className="inline">
{t("occurrence", {
count: recurringEventCount,
@@ -227,14 +248,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
)}
{paymentAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>
{t("no_show_fee_amount", {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
})}
</>
) : (
<IntlProvider locale="en">
<FormattedNumber
value={paymentAppData.price / 100.0}
style="currency"
currency={paymentAppData.currency?.toUpperCase()}
/>
</IntlProvider>
)}
</p>
)}
{timezoneDropdown}
@@ -243,12 +273,12 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
className="mt-4 mb-3 text-default"
data-testid="former_time_p_desktop">
{t("former_time")}
</p>
<p className="text-gray-500 line-through dark:text-darkgray-600">
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
<p className="text-subtle line-through ">
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-subtle" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
@@ -11,7 +11,6 @@ import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import type { LocationObject } from "@calcom/core/location";
@@ -29,19 +28,22 @@ import {
import getBookingResponsesSchema, {
getBookingResponsesPartialSchema,
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
import CustomBranding from "@calcom/lib/CustomBranding";
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
import { APP_NAME, MINUTES_TO_BOOK } from "@calcom/lib/constants";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { Button, Form, Tooltip } from "@calcom/ui";
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
import { trpc } from "@calcom/trpc";
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -64,6 +66,14 @@ const BookingDescriptionPayment = dynamic(
() => import("@components/booking/BookingDescriptionPayment")
) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default;
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
const BookingFields = ({
fields,
@@ -135,26 +145,14 @@ const BookingFields = ({
}
const optionsInputs = field.optionsInputs;
const options = locations.map((location) => {
const eventLocation = getEventLocationType(location.type);
const locationString = locationKeyToString(location);
if (typeof locationString !== "string" || !eventLocation) {
// It's possible that location app got uninstalled
return null;
}
const type = eventLocation.type;
const optionInput = optionsInputs[type as keyof typeof optionsInputs];
// TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field.
const options = getLocationOptionsForSelect(locations, t);
options.forEach((option) => {
const optionInput = optionsInputs[option.value as keyof typeof optionsInputs];
if (optionInput) {
optionInput.placeholder = t(eventLocation?.attendeeInputPlaceholder || "");
optionInput.placeholder = option.inputPlaceholder;
}
return {
label: t(locationString),
value: type,
};
});
field.options = options.filter(
(location): location is NonNullable<(typeof options)[number]> => !!location
);
@@ -213,8 +211,11 @@ const BookingPage = ({
hashedLink,
...restProps
}: BookingPageProps) => {
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const { t, i18n } = useLocale();
const { duration: queryDuration } = useRouterQuery("duration");
const { date: queryDate } = useRouterQuery("date");
const isEmbed = useIsEmbed(restProps.isEmbed);
const embedUiConfig = useEmbedUiConfig();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
@@ -230,6 +231,15 @@ const BookingPage = ({
}),
{}
);
const reserveSlot = () => {
if (queryDuration) {
reserveSlotMutation.mutate({
eventTypeId: eventType.id,
slotUtcStartDate: dayjs(queryDate).utc().format(),
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
});
}
};
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (
@@ -242,13 +252,19 @@ const BookingPage = ({
}
useEffect(() => {
if (top !== window) {
/* if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
);
}
} */
reserveSlot();
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
return () => {
clearInterval(interval);
removeSelectedSlotMarkMutation.mutate();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -268,15 +284,19 @@ const BookingPage = ({
);
}
return router.push({
pathname: `/booking/${uid}`,
query: {
isSuccessBookingPage: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
},
const query = {
isSuccessBookingPage: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
@@ -284,16 +304,18 @@ const BookingPage = ({
const recurringMutation = useMutation(createRecurringBooking, {
onSuccess: async (responseData = []) => {
const { uid } = responseData[0] || {};
return router.push({
pathname: `/booking/${uid}`,
query: {
isSuccessBookingPage: true,
allRemainingBookings: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
},
const query = {
isSuccessBookingPage: true,
allRemainingBookings: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
@@ -303,6 +325,10 @@ const BookingPage = ({
} = useTypedQuery(routerQuerySchema);
useTheme(profile.theme);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const querySchema = getBookingResponsesPartialSchema({
eventType: {
@@ -512,7 +538,6 @@ const BookingPage = ({
<link rel="icon" href="/favico.ico" />
</Head>
<BookingPageTagManager eventType={eventType} />
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
@@ -522,17 +547,17 @@ const BookingPage = ({
<div
className={classNames(
"main",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
"dark:border-darkgray-300 rounded-md sm:border"
isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
"border-booker sm:border-booker-width rounded-md"
)}>
<div className="sm:flex">
{showEventTypeDetails && (
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
<BookingDescriptionPayment eventType={eventType} />
<BookingDescriptionPayment eventType={eventType} t={t} />
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="dark:text-inverted text-default items-start text-sm font-medium">
<RefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({
t,
@@ -543,7 +568,7 @@ const BookingPage = ({
</div>
)}
<div className="text-bookinghighlight flex items-start text-sm">
<FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<Calendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="text-sm font-medium">
{isClientTimezoneAvailable &&
(rescheduleUid || !eventType.recurringEvent?.freq) &&
@@ -560,7 +585,7 @@ const BookingPage = ({
content={recurringStrings.slice(5).map((timeFormatted, key) => (
<p key={key}>{timeFormatted}</p>
))}>
<p className="dark:text-darkgray-600 text-sm">
<p className=" text-sm">
+ {t("plus_more", { count: recurringStrings.length - 5 })}
</p>
</Tooltip>
@@ -574,7 +599,7 @@ const BookingPage = ({
{t("former_time")}
</p>
<p className="line-through ">
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{isClientTimezoneAvailable &&
typeof booking.startTime === "string" &&
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
@@ -583,7 +608,7 @@ const BookingPage = ({
)}
{!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm">
<FiUser
<User
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
@@ -637,16 +662,15 @@ const BookingPage = ({
</Button>
<Button
type="submit"
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast"
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
loading={mutation.isLoading || recurringMutation.isLoading}>
{rescheduleUid ? t("reschedule") : t("confirm")}
</Button>
</div>
</Form>
{(mutation.isError || recurringMutation.isError) && (
{mutation.isError || recurringMutation.isError ? (
<ErrorMessage error={mutation.error || recurringMutation.error} />
)}
) : null}
</div>
</div>
</div>
@@ -667,9 +691,9 @@ function ErrorMessage({ error }: { error: unknown }) {
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
<AlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
</div>
<div className="ltr:ml-3 rtl:mr-3">
<div className="ms-3">
<p className="text-sm text-blue-700">
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
{error instanceof HttpError || error instanceof Error ? (
@@ -0,0 +1,100 @@
import { Trans } from "next-i18next";
import { useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import { IntlProvider, FormattedNumber } from "react-intl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
showToast,
} from "@calcom/ui";
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
interface IRescheduleDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingId: number;
paymentAmount: number;
paymentCurrency: string;
}
export const ChargeCardDialog = (props: IRescheduleDialog) => {
const { t } = useLocale();
const utils = trpc.useContext();
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
const [chargeError, setChargeError] = useState(false);
const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
onSuccess: () => {
utils.viewer.bookings.invalidate();
setIsOpenDialog(false);
showToast("Charge successful", "success");
},
onError: () => {
setChargeError(true);
},
});
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<CreditCard className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("charge_card")} />
<Trans i18nKey="charge_card_dialog_body">
<p className="text-sm text-gray-500">
You are about to charge the attendee{" "}
<IntlProvider locale="en">
<FormattedNumber
value={props.paymentAmount / 100.0}
style="currency"
currency={props.paymentCurrency?.toUpperCase()}
/>
</IntlProvider>
. Are you sure you want to continue?
</p>
</Trans>
{chargeError && (
<div className="mt-4 flex text-red-500">
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
<p className="text-sm">{t("error_charging_card")}</p>
</div>
)}
<DialogFooter>
<DialogClose />
<Button
data-testid="send_request"
disabled={chargeCardMutation.isLoading || chargeError}
onClick={() =>
chargeCardMutation.mutate({
bookingId,
})
}>
<Trans i18nKey="charge_card_confirm">
Charge attendee{" "}
<IntlProvider locale="en">
<FormattedNumber
value={props.paymentAmount / 100.0}
style="currency"
currency={props.paymentCurrency?.toUpperCase()}
/>
</IntlProvider>
</Trans>
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
);
};
@@ -19,7 +19,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Input } from "@calcom/ui";
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
import { FiMapPin } from "@calcom/ui/components/icon";
import { MapPin } from "@calcom/ui/components/icon";
import { QueryCell } from "@lib/QueryCell";
@@ -164,7 +164,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
return (
<div>
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
<label htmlFor="locationInput" className="text-default block text-sm font-medium">
{t(eventLocationType.messageForOrganizer || "")}
</label>
<div className="mt-1">
@@ -174,6 +174,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required
className="border-default block w-full rounded-sm text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
@@ -182,7 +183,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<ErrorMessage
errors={locationFormMethods.formState.errors}
name={eventLocationType.variable}
className="mt-1 text-sm text-red-500"
className="text-error mt-1 text-sm"
as="p"
/>
</div>
@@ -208,7 +209,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
</div>
);
} else {
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
return <p className="text-default text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
}
})();
@@ -216,16 +217,16 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
<DialogContent>
<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">
<FiMapPin className="text-primary-600 h-6 w-6" />
<div className="bg-subtle 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">
<MapPin className="text-emphasis h-6 w-6" />
</div>
<div className="w-full">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
{t("edit_location")}
</h3>
{!booking && (
<p className="text-sm text-gray-400">
<p className="text-default text-sm">
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
Can&apos;t find the right video app? Visit our
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
@@ -240,8 +241,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
{booking && (
<>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black">
<p className="text-emphasis mt-6 mb-2 ml-1 text-sm font-bold">{t("current_location")}:</p>
<p className="text-emphasis mb-2 ml-1 text-sm">
{getHumanReadableLocationValue(booking.location, t)}
</p>
</>
@@ -13,7 +13,7 @@ import {
showToast,
TextArea,
} from "@calcom/ui";
import { FiClock } from "@calcom/ui/components/icon";
import { Clock } from "@calcom/ui/components/icon";
interface IRescheduleDialog {
isOpenDialog: boolean;
@@ -44,14 +44,14 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<FiClock className="m-auto h-6 w-6" />
<Clock className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} />
<p className="text-sm text-gray-500">{t("reschedule_modal_description")}</p>
<p className="mt-6 mb-2 text-sm font-bold text-black">
<p className="text-subtle text-sm">{t("reschedule_modal_description")}</p>
<p className="text-emphasis mt-6 mb-2 text-sm font-bold">
{t("reason_for_reschedule_request")}
<span className="font-normal text-gray-500"> (Optional)</span>
<span className="text-subtle font-normal"> (Optional)</span>
</p>
<TextArea
data-testid="reschedule_reason"
+8 -8
View File
@@ -29,15 +29,15 @@ const ErrorDebugPanel: React.FC<{ error: Props["error"]; children?: never }> = (
];
return (
<div className="overflow-hidden bg-white shadow sm:rounded-lg">
<div className="border-t border-gray-200 px-4 py-5 sm:p-0">
<dl className="sm:divide-y sm:divide-gray-200">
<div className="bg-default overflow-hidden shadow sm:rounded-lg">
<div className="border-subtle border-t px-4 py-5 sm:p-0">
<dl className="sm:divide-subtle sm:divide-y">
{debugMap.map(([key, value]) => {
if (value !== undefined) {
return (
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
<dt className="text-sm font-bold text-black">{key}</dt>
<dd className="mt-1 text-sm text-black sm:col-span-2 sm:mt-0">{value}</dd>
<dt className="text-emphasis text-sm font-bold">{key}</dt>
<dd className="text-emphasis mt-1 text-sm sm:col-span-2 sm:mt-0">{value}</dd>
</div>
);
}
@@ -53,11 +53,11 @@ export const ErrorPage: React.FC<Props> = (props) => {
return (
<>
<div className="min-h-screen bg-white px-4">
<div className="bg-default min-h-screen px-4">
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
<div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide text-black">{statusCode}</p>
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl">
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">{statusCode}</p>
<h1 className="text-emphasis mt-2 text-4xl font-extrabold tracking-tight sm:text-5xl">
{message}
</h1>
</div>
@@ -58,35 +58,35 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
className="mb-0"
/>
<div className="pt-6 text-sm">
<div className="mb-6 rounded-md bg-gray-100 p-2">
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
<div className="bg-subtle mb-6 rounded-md p-2">
<h1 className="text-emphasis mb-2 ml-1 font-medium">{t("available_variables")}</h1>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
<p className="text-gray-900">{t("event_name_info")}</p>
<p className="text-subtle ml-1 mr-5 w-28">{`{Event type title}`}</p>
<p className="text-emphasis">{t("event_name_info")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
<p className="text-gray-900">{t("your_full_name")}</p>
<p className="text-subtle ml-1 mr-5 w-28">{`{Organiser}`}</p>
<p className="text-emphasis">{t("your_full_name")}</p>
</div>
<div className="mb-2.5 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
<p className="text-gray-900">{t("scheduler_full_name")}</p>
<p className="text-subtle ml-1 mr-5 w-28">{`{Scheduler}`}</p>
<p className="text-emphasis">{t("scheduler_full_name")}</p>
</div>
<div className="mb-1 flex font-normal">
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
<p className="text-gray-900">{t("location_info")}</p>
<p className="text-subtle ml-1 mr-5 w-28">{`{Location}`}</p>
<p className="text-emphasis">{t("location_info")}</p>
</div>
</div>
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
<div
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center dark:invert"
style={{
backgroundImage: "url(/calendar-preview.svg)",
}}>
<div className="m-auto flex items-center justify-center self-stretch">
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">{previewText}</p>
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
<div className="bg-subtle mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black text-[12px] leading-3">
<p className="text-emphasis overflow-hidden text-ellipsis p-1.5 font-medium">{previewText}</p>
<p className="text-default ml-1.5 text-[10px] font-normal">8 - 10 AM</p>
</div>
</div>
</div>
@@ -8,15 +8,17 @@ import { v5 as uuidv5 } from "uuid";
import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
import { Edit, Copy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -57,6 +59,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
useEffect(() => {
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
@@ -78,11 +81,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
);
};
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const eventNamePlaceholder = getEventName({
...eventNameObject,
eventName: formMethods.watch("eventName"),
});
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
const closeEventNameTip = () => setShowEventNameTip(false);
const setEventName = (value: string) => formMethods.setValue("eventName", value);
@@ -100,7 +111,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Link
href="/apps/categories/calendar"
target="_blank"
className="text-sm text-gray-600 hover:text-gray-900">
className="hover:text-emphasis text-default text-sm">
{t("add_another_calendar")}
</Link>
</div>
@@ -119,44 +130,51 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)}
/>
</div>
<p className="text-sm text-gray-600">{t("select_which_cal")}</p>
<p className="text-default text-sm">{t("select_which_cal")}</p>
</div>
)}
<div className="w-full">
<TextField
label={t("event_name_in_calendar")}
type="text"
{...shouldLockDisableProps("eventName")}
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
addOnSuffix={
<Button
type="button"
StartIcon={FiEdit}
variant="icon"
color="minimal"
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
onClick={() => setShowEventNameTip((old) => !old)}
size="sm"
aria-label="edit custom name"
/>
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
onClick={() => setShowEventNameTip((old) => !old)}>
<Edit className="h-4 w-4" />
</Button>
}
/>
</div>
<hr />
<hr className="border-subtle" />
<FormBuilder
title={t("booking_questions_title")}
description={t("booking_questions_description")}
addFieldLabel={t("add_a_booking_question")}
formProp="bookingFields"
{...shouldLockDisableProps("bookingFields")}
dataStore={{
options: {
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
},
}}
/>
<hr />
<hr className="border-subtle" />
<RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled}
metadata={eventType.metadata}
requiresConfirmation={requiresConfirmation}
onRequiresConfirmation={setRequiresConfirmation}
/>
<hr />
<hr className="border-subtle" />
<Controller
name="hideCalendarNotes"
control={formMethods.control}
@@ -164,13 +182,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
render={({ field: { value, onChange } }) => (
<SettingsToggle
title={t("disable_notes")}
{...shouldLockDisableProps("hideCalendarNotes")}
description={t("disable_notes_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
)}
/>
<hr />
<hr className="border-subtle" />
<Controller
name="successRedirectUrl"
control={formMethods.control}
@@ -178,6 +197,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<>
<SettingsToggle
title={t("redirect_success_booking")}
{...successRedirectUrlLocked}
description={t("redirect_url_description")}
checked={redirectUrlVisible}
onCheckedChange={(e) => {
@@ -190,6 +210,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
className="w-full"
label={t("redirect_success_booking")}
labelSrOnly
disabled={successRedirectUrlLocked.disabled}
placeholder={t("external_redirect_url")}
required={redirectUrlVisible}
type="text"
@@ -208,10 +229,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</>
)}
/>
<hr />
<hr className="border-subtle" />
<SettingsToggle
data-testid="hashedLinkCheck"
title={t("private_link")}
{...shouldLockDisableProps("hashedLinkCheck")}
description={t("private_link_description", { appName: APP_NAME })}
checked={hashedLinkVisible}
onCheckedChange={(e) => {
@@ -233,6 +255,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button
color="minimal"
size="sm"
type="button"
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
@@ -240,70 +266,76 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
} else {
showToast(t("enabled_after_update_description"), "warning");
}
}}
className="hover:stroke-3 hover:bg-transparent hover:text-black"
type="button">
<FiCopy />
}}>
<Copy className="h-4 w-4" />
</Button>
</Tooltip>
}
/>
</div>
</SettingsToggle>
<hr />
<hr className="border-subtle" />
<Controller
name="seatsPerTimeSlotEnabled"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<SettingsToggle
data-testid="offer-seats-toggle"
title={t("offer_seats")}
description={t("offer_seats_description")}
checked={value}
onCheckedChange={(e) => {
// Enabling seats will disable guests and requiring confirmation until fully supported
if (e) {
toggleGuests(false);
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
}
onChange(e);
}}>
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div className="lg:-ml-2">
<TextField
required
name="seatsPerTimeSlot"
labelSrOnly
label={t("number_of_seats")}
type="number"
defaultValue={value || 2}
min={1}
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
}}
/>
<div className="mt-2">
<Checkbox
description={t("show_attendees")}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
<>
<SettingsToggle
data-testid="offer-seats-toggle"
title={t("offer_seats")}
{...seatsLocked}
description={t("offer_seats_description")}
checked={value}
disabled={noShowFeeEnabled}
onCheckedChange={(e) => {
// Enabling seats will disable guests and requiring confirmation until fully supported
if (e) {
toggleGuests(false);
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
toggleGuests(true);
}
onChange(e);
}}>
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div className="lg:-ml-2">
<TextField
required
name="seatsPerTimeSlot"
labelSrOnly
label={t("number_of_seats")}
type="number"
disabled={seatsLocked.disabled}
defaultValue={value || 2}
min={1}
className="w-24"
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
}}
/>
<div className="mt-2">
<Checkbox
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
/>
</div>
</div>
</div>
)}
/>
</SettingsToggle>
)}
/>
</SettingsToggle>
{noShowFeeEnabled && <Alert severity="warning" title={t("seats_and_no_show_fee_error")} />}
</>
)}
/>
+44 -21
View File
@@ -5,10 +5,11 @@ import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppConte
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen } from "@calcom/ui";
import { FiGrid } from "@calcom/ui/components/icon";
import { Button, EmptyScreen, Alert } from "@calcom/ui";
import { Grid, Lock } from "@calcom/ui/components/icon";
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"];
@@ -55,19 +56,39 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
};
};
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
return (
<>
<div>
<div className="before:border-0">
{!installedApps?.length && isManagedEventType && (
<Alert
severity="neutral"
className="mb-2"
title={t("locked_for_members")}
message={t("locked_apps_description")}
/>
)}
{!isLoading && !installedApps?.length ? (
<EmptyScreen
Icon={FiGrid}
Icon={Grid}
headline={t("empty_installed_apps_headline")}
description={t("empty_installed_apps_description")}
buttonRaw={
<Button target="_blank" color="secondary" href="/apps">
{t("empty_installed_apps_button")}{" "}
</Button>
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
<Button target="_blank" color="secondary" href="/apps">
{t("empty_installed_apps_button")}{" "}
</Button>
)
}
/>
) : null}
@@ -82,22 +103,24 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
))}
</div>
</div>
<div>
{!isLoading && notInstalledApps?.length ? (
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">Available Apps</h2>
) : null}
<div className="before:border-0">
{notInstalledApps?.map((app) => (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
app={app}
eventType={eventType}
/>
))}
{!shouldLockDisableProps("apps").disabled && (
<div>
{!isLoading && notInstalledApps?.length ? (
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">{t("available_apps")}</h2>
) : null}
<div className="before:border-0">
{notInstalledApps?.map((app) => (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug}
app={app}
eventType={eventType}
/>
))}
</div>
</div>
</div>
)}
</>
);
};
@@ -1,29 +1,29 @@
import type { FormValues } from "pages/event-types/[type]";
import { SchedulingType } from "@prisma/client";
import type { FormValues, EventTypeSetup } from "pages/event-types/[type]";
import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select";
import dayjs from "@calcom/dayjs";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { NewScheduleButton } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
type AvailabilityOption = {
label: string;
value: number;
isDefault: boolean;
isManaged?: boolean;
};
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
const { label, isDefault, isManaged = false } = props.data;
const { t } = useLocale();
return (
<components.Option {...props}>
@@ -33,12 +33,17 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
{t("default")}
</Badge>
)}
{isManaged && (
<Badge variant="gray" className="ml-2">
{t("managed")}
</Badge>
)}
</components.Option>
);
};
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
const { label, isDefault, isManaged = false } = props.data;
const { t } = useLocale();
return (
<components.SingleValue {...props}>
@@ -48,50 +53,39 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
{t("default")}
</Badge>
)}
{isManaged && (
<Badge variant="gray" className="ml-2">
{t("managed")}
</Badge>
)}
</components.SingleValue>
);
};
const AvailabilitySelect = ({
className = "",
isLoading,
schedules,
options,
value,
...props
}: {
className?: string;
name: string;
value: number;
isLoading: boolean;
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
value: AvailabilityOption | undefined;
options: AvailabilityOption[];
isDisabled?: boolean;
onBlur: () => void;
onChange: (value: AvailabilityOption | null) => void;
}) => {
const { t } = useLocale();
if (isLoading) {
return <SelectSkeletonLoader />;
}
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
}));
const value = options.find((option) =>
props.value
? option.value === props.value
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
return (
<Select
placeholder={t("select")}
options={options}
isDisabled={props.isDisabled}
isSearchable={false}
onChange={props.onChange}
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
value={value}
defaultValue={value}
components={{ Option, SingleValue }}
isMulti={false}
/>
@@ -103,24 +97,32 @@ const format = (date: Date, hour12: boolean) =>
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
);
const EventTypeScheduleDetails = () => {
const EventTypeScheduleDetails = ({
isManagedEventType,
selectedScheduleValue,
}: {
isManagedEventType: boolean;
selectedScheduleValue: AvailabilityOption | undefined;
}) => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
{
scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
);
const filterDays = (dayNum: number) =>
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
return (
<div className="space-y-4 rounded border px-6 pb-4">
<div className="border-default space-y-4 rounded border px-6 pb-4">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
@@ -128,8 +130,8 @@ const EventTypeScheduleDetails = () => {
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32",
!isAvailable && "text-gray-500 opacity-50"
"w-20 font-medium sm:w-32 ",
!isAvailable ? "text-subtle line-through" : "text-default"
)}>
{day}
</span>
@@ -138,34 +140,34 @@ const EventTypeScheduleDetails = () => {
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="flex items-center leading-4">
<div key={i} className="text-default flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ltr:ml-4 rtl:mr-4">-</span>
<span className="ms-4">-</span>
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr />
<hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && (
{!!schedule?.id && !schedule.isManaged && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={FiExternalLink}
EndIcon={ExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
@@ -176,14 +178,23 @@ const EventTypeScheduleDetails = () => {
);
};
const EventTypeSchedule = () => {
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const { data: schedules, isLoading } = trpc.viewer.availability.list.useQuery();
const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } =
useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const { watch } = useFormContext<FormValues>();
const watchSchedule = watch("schedule");
if (!schedules?.schedules.length && !isLoading)
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
if (!data?.schedules.length && !isLoading)
return (
<EmptyScreen
Icon={FiClock}
Icon={Clock}
headline={t("new_schedule_heading")}
description={t("new_schedule_description")}
buttonRaw={<NewScheduleButton fromEventType />}
@@ -191,22 +202,65 @@ const EventTypeSchedule = () => {
/>
);
const schedules = data?.schedules || [];
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
isManaged: false,
}));
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
if (isManagedEventType) {
options.push({
value: 0,
label: t("members_default_schedule"),
isDefault: false,
isManaged: false,
});
}
// We are showing a managed event for a member and team owner selected their own schedule, so adding
// the managed schedule option
if (
isChildrenManagedEventType &&
watchSchedule &&
!schedules.find((schedule) => schedule.id === watchSchedule)
) {
options.push({
value: watchSchedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
isDefault: false,
isManaged: false,
});
}
const value = options.find((option) =>
watchSchedule
? option.value === watchSchedule
: isManagedEventType
? option.value === 0
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
return (
<div className="space-y-4">
<div>
<label htmlFor="availability" className="mb-2 block text-sm font-medium leading-none text-gray-700">
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
{t("availability")}
{shouldLockIndicator("availability")}
</label>
<Controller
name="schedule"
render={({ field }) => (
<>
<AvailabilitySelect
value={field.value}
value={value}
options={options}
onBlur={field.onBlur}
isDisabled={shouldLockDisableProps("schedule").disabled}
name={field.name}
schedules={schedules?.schedules || []}
isLoading={isLoading}
onChange={(selected) => {
field.onChange(selected?.value || null);
}}
@@ -215,12 +269,21 @@ const EventTypeSchedule = () => {
)}
/>
</div>
<EventTypeScheduleDetails />
{value?.value !== 0 ? (
<EventTypeScheduleDetails
selectedScheduleValue={value}
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
/>
) : (
isManagedEventType && (
<p className="!mt-2 ml-1 text-sm text-gray-600">{t("members_default_schedule_description")}</p>
)
)}
</div>
);
};
const UseCommonScheduleSettingsToggle = () => {
const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const { resetField, setValue } = useFormContext<FormValues>();
return (
@@ -239,13 +302,23 @@ const UseCommonScheduleSettingsToggle = () => {
}}
title={t("choose_common_schedule_team_event")}
description={t("choose_common_schedule_team_event_description")}>
<EventTypeSchedule />
<EventTypeSchedule eventType={eventType} />
</SettingsToggle>
)}
/>
);
};
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
export const EventAvailabilityTab = ({
eventType,
isTeamEvent,
}: {
eventType: EventTypeSetup;
isTeamEvent: boolean;
}) => {
return isTeamEvent && eventType.schedulingType !== SchedulingType.MANAGED ? (
<UseCommonScheduleSettingsToggle eventType={eventType} />
) : (
<EventTypeSchedule eventType={eventType} />
);
};
+104 -48
View File
@@ -7,6 +7,7 @@ import type { UseFormRegisterReturn } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { SingleValue } from "react-select";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { classNames } from "@calcom/lib";
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
@@ -14,17 +15,8 @@ import findDurationType from "@calcom/lib/findDurationType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { PeriodType } from "@calcom/prisma/client";
import type { IntervalLimit } from "@calcom/types/Calendar";
import {
Button,
DateRangePicker,
Input,
InputField,
Label,
Select,
SettingsToggle,
TextField,
} from "@calcom/ui";
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
import { Plus, Trash } from "@calcom/ui/components/icon";
const MinimumBookingNoticeInput = React.forwardRef<
HTMLInputElement,
@@ -78,6 +70,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
<div className="w-1/2 md:w-full">
<InputField
required
disabled={passThroughProps.disabled}
defaultValue={minimumBookingNoticeDisplayValues.value}
onChange={(e) =>
setMinimumBookingNoticeDisplayValues({
@@ -95,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
</div>
<Select
isSearchable={false}
isDisabled={passThroughProps.disabled}
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
@@ -146,12 +140,30 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
defaultValue: periodType?.type,
});
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
const periodTypeLocked = shouldLockDisableProps("periodType");
const optionsPeriod = [
{ value: 1, label: t("calendar_days") },
{ value: 0, label: t("business_days") },
];
return (
<div className="space-y-8">
<div className="space-y-4 lg:space-y-8">
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full">
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
<Label htmlFor="beforeBufferTime">
{t("before_event")}
{shouldLockIndicator("bookingLimits")}
</Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
@@ -170,6 +182,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
onChange={(val) => {
if (val) onChange(val.value);
}}
@@ -183,7 +196,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
<Label htmlFor="afterBufferTime">
{t("after_event")}
{shouldLockIndicator("bookingLimits")}
</Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
@@ -202,6 +218,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
onChange={(val) => {
if (val) onChange(val.value);
}}
@@ -217,11 +234,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full">
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
<Label htmlFor="minimumBookingNotice">
{t("minimum_booking_notice")}
{shouldLockIndicator("minimumBookingNotice")}
</Label>
<MinimumBookingNoticeInput
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
{...formMethods.register("minimumBookingNotice")}
/>
</div>
<div className="w-full">
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
<Label htmlFor="slotInterval">
{t("slot_interval")}
{shouldLockIndicator("slotInterval")}
</Label>
<Controller
name="slotInterval"
control={formMethods.control}
@@ -239,6 +265,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
return (
<Select
isSearchable={false}
isDisabled={shouldLockDisableProps("slotInterval").disabled}
onChange={(val) => {
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}}
@@ -254,13 +281,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
</div>
</div>
<hr />
<hr className="border-subtle" />
<Controller
name="bookingLimits"
control={formMethods.control}
render={({ field: { value } }) => (
<SettingsToggle
title={t("limit_booking_frequency")}
{...bookingLimitsLocked}
description={t("limit_booking_frequency_description")}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
@@ -272,11 +300,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
formMethods.setValue("bookingLimits", {});
}
}}>
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
<IntervalLimitsManager
disabled={bookingLimitsLocked.disabled}
propertyName="bookingLimits"
defaultLimit={1}
step={1}
/>
</SettingsToggle>
)}
/>
<hr />
<hr className="border-subtle" />
<Controller
name="durationLimits"
control={formMethods.control}
@@ -284,6 +317,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<SettingsToggle
title={t("limit_total_booking_duration")}
description={t("limit_total_booking_duration_description")}
{...durationLimitsLocked}
checked={Object.keys(value ?? {}).length > 0}
onCheckedChange={(active) => {
if (active) {
@@ -297,13 +331,14 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<IntervalLimitsManager
propertyName="durationLimits"
defaultLimit={60}
disabled={durationLimitsLocked.disabled}
step={15}
textFieldSuffix={t("minutes")}
/>
</SettingsToggle>
)}
/>
<hr />
<hr className="border-subtle" />
<Controller
name="periodType"
control={formMethods.control}
@@ -311,49 +346,64 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<SettingsToggle
title={t("limit_future_bookings")}
description={t("limit_future_bookings_description")}
{...periodTypeLocked}
checked={value && value !== "UNLIMITED"}
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
<RadioGroup.Root
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
{PERIOD_TYPES.map((period) => {
{PERIOD_TYPES.filter((opt) =>
periodTypeLocked.disabled ? watchPeriodType === opt.type : true
).map((period) => {
if (period.type === "UNLIMITED") return null;
return (
<div
className={classNames(
"mb-2 flex flex-wrap items-center text-sm",
"text-default mb-2 flex flex-wrap items-center text-sm",
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
)}
key={period.type}>
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
{!periodTypeLocked.disabled && (
<RadioGroup.Item
id={period.type}
value={period.type}
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
</RadioGroup.Item>
)}
{period.prefix ? <span>{period.prefix}&nbsp;</span> : null}
{period.type === "ROLLING" && (
<div className="flex h-9">
<Input
<div className="flex items-center">
<TextField
labelSrOnly
type="number"
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
placeholder="30"
disabled={periodTypeLocked.disabled}
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<select
id=""
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
{...formMethods.register("periodCountCalendarDays")}
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
<option value="1">{t("calendar_days")}</option>
<option value="0">{t("business_days")}</option>
</select>
<Select
options={optionsPeriod}
isSearchable={false}
isDisabled={periodTypeLocked.disabled}
onChange={(opt) => {
formMethods.setValue(
"periodCountCalendarDays",
opt?.value.toString() as "0" | "1"
);
}}
defaultValue={
optionsPeriod.find(
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
) ?? optionsPeriod[0]
}
/>
</div>
)}
{period.type === "RANGE" && (
<div className="inline-flex space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
<div className="ms-2 me-2 inline-flex space-x-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
@@ -362,6 +412,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
endDate={formMethods.getValues("periodDates").endDate}
disabled={periodTypeLocked.disabled}
onDatesChange={({ startDate, endDate }) => {
formMethods.setValue("periodDates", {
startDate,
@@ -373,9 +424,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
</div>
)}
{period.suffix ? (
<span className="ltr:ml-2 ltr:mr-2 rtl:ml-2">&nbsp;{period.suffix}</span>
) : null}
{period.suffix ? <span className="ms-2 me-2">&nbsp;{period.suffix}</span> : null}
</div>
);
})}
@@ -402,6 +451,7 @@ type IntervalLimitItemProps = {
step: number;
value: number;
textFieldSuffix?: string;
disabled?: boolean;
selectOptions: { value: keyof IntervalLimit; label: string }[];
hasDeleteButton?: boolean;
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
@@ -416,6 +466,7 @@ const IntervalLimitItem = ({
textFieldSuffix,
selectOptions,
hasDeleteButton,
disabled,
onDelete,
onLimitChange,
onIntervalSelect,
@@ -426,8 +477,9 @@ const IntervalLimitItem = ({
required
type="number"
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
className="mb-0"
className="mb-0 !h-auto"
placeholder={`${value}`}
disabled={disabled}
min={step}
step={step}
defaultValue={value}
@@ -437,11 +489,12 @@ const IntervalLimitItem = ({
<Select
options={selectOptions}
isSearchable={false}
isDisabled={disabled}
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
onChange={onIntervalSelect}
/>
{hasDeleteButton && (
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
{hasDeleteButton && !disabled && (
<Button variant="icon" StartIcon={Trash} color="destructive" onClick={() => onDelete(limitKey)} />
)}
</div>
);
@@ -452,6 +505,7 @@ type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> =
defaultLimit: number;
step: number;
textFieldSuffix?: string;
disabled?: boolean;
};
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
@@ -459,6 +513,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
defaultLimit,
step,
textFieldSuffix,
disabled,
}: IntervalLimitsManagerProps<K>) => {
const { watch, setValue, control } = useFormContext<FormValues>();
const watchIntervalLimits = watch(propertyName);
@@ -508,6 +563,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
limitKey={limitKey}
step={step}
value={value}
disabled={disabled}
textFieldSuffix={textFieldSuffix}
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
@@ -538,8 +594,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
/>
);
})}
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
<Button color="minimal" StartIcon={Plus} onClick={addLimit}>
{t("add_limit")}
</Button>
)}
@@ -11,7 +11,7 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
return (
<div className="">
<RecurringEventController paymentEnabled={requirePayment} recurringEvent={eventType.recurringEvent} />
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
</div>
);
};
+96 -29
View File
@@ -11,6 +11,7 @@ import { z } from "zod";
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
@@ -27,7 +28,7 @@ import {
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
import { Edit2, Check, X, Plus } from "@calcom/ui/components/icon";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
@@ -45,8 +46,33 @@ const getLocationFromType = (
}
};
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const locationAvailable =
props.eventType.locations &&
props.eventType.locations.length > 0 &&
props.locationOptions.some((op) =>
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
);
const locationDetails = props.eventType.locations &&
props.eventType.locations.length > 0 &&
!locationAvailable && {
slug: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", "-")
.replace("_video", ""),
name: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", " ")
.replace("_video", "")
.split(" ")
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(" "),
};
return { locationAvailable, locationDetails };
};
interface DescriptionEditorProps {
description?: string | null;
editable?: boolean;
}
const DescriptionEditor = (props: DescriptionEditorProps) => {
@@ -64,6 +90,7 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={props.editable}
/>
) : (
<SkeletonContainer>
@@ -174,6 +201,13 @@ export const EventSetupTab = (
resolver: zodResolver(locationFormSchema),
});
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const Locations = () => {
const { t } = useLocale();
@@ -188,6 +222,12 @@ export const EventSetupTab = (
return true;
});
const defaultValue = isManagedEventType
? locationOptions.find((op) => op.label === t("default"))?.options[0]
: undefined;
const { locationDetails, locationAvailable } = getLocationInfo(props);
return (
<div className="w-full">
{validLocations.length === 0 && (
@@ -195,6 +235,8 @@ export const EventSetupTab = (
<LocationSelect
placeholder={t("select")}
options={locationOptions}
isDisabled={shouldLockDisableProps("locations").disabled}
defaultValue={defaultValue}
isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
menuPlacement="auto"
@@ -230,7 +272,7 @@ export const EventSetupTab = (
return (
<li
key={`${location.type}${index}`}
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
className="border-default text-default mb-2 rounded-md border py-1.5 px-2 hover:cursor-pointer">
<div className="flex items-center justify-between">
<div className="flex items-center">
<img
@@ -238,7 +280,7 @@ export const EventSetupTab = (
className="h-4 w-4"
alt={`${eventLocationType.label} logo`}
/>
<span className="line-clamp-1 text-sm ltr:ml-1 rtl:mr-1">{eventLabel}</span>
<span className="line-clamp-1 ms-1 text-sm">{eventLabel}</span>
</div>
<div className="flex">
<button
@@ -252,11 +294,11 @@ export const EventSetupTab = (
openLocationModal(location.type);
}}
aria-label={t("edit")}
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
<FiEdit2 className="h-4 w-4" />
className="hover:text-emphasis text-subtle mr-1 p-1">
<Edit2 className="h-4 w-4" />
</button>
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
<FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
<X className="border-l-1 hover:text-emphasis text-subtle h-6 w-6 pl-1 " />
</button>
</div>
</div>
@@ -267,8 +309,8 @@ export const EventSetupTab = (
(location) =>
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
) && (
<div className="flex text-sm text-gray-600">
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
<div className="text-default flex text-sm">
<Check 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.
@@ -282,11 +324,19 @@ export const EventSetupTab = (
</Trans>
</div>
)}
{validLocations.length > 0 && (
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
<p className="pl-1 text-sm leading-none text-red-600">
{t("app_not_connected", { appName: locationDetails.name })}{" "}
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
{t("connect_now")}
</a>
</p>
)}
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
<li>
<Button
data-testid="add-location"
StartIcon={FiPlus}
StartIcon={Plus}
color="minimal"
onClick={() => setShowLocationModal(true)}>
{t("add_location")}
@@ -299,22 +349,33 @@ export const EventSetupTab = (
);
};
const lengthLockedProps = shouldLockDisableProps("length");
const descriptionLockedProps = shouldLockDisableProps("description");
return (
<div>
<div className="space-y-8">
<TextField
required
label={t("title")}
{...shouldLockDisableProps("title")}
defaultValue={eventType.title}
{...formMethods.register("title")}
/>
<div>
<Label>{t("description")}</Label>
<DescriptionEditor description={eventType?.description} />
<Label>
{t("description")}
{shouldLockIndicator("description")}
</Label>
<DescriptionEditor
description={eventType?.description}
editable={!descriptionLockedProps.disabled}
/>
</div>
<TextField
required
label={t("URL")}
{...shouldLockDisableProps("slug")}
defaultValue={eventType.slug}
addOnLeading={
<>
@@ -367,12 +428,14 @@ export const EventSetupTab = (
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("default_duration")}
{shouldLockIndicator("length")}
</Skeleton>
<Select
value={defaultDuration}
isSearchable={false}
name="length"
className="text-sm"
isDisabled={lengthLockedProps.disabled}
noOptionsMessage={() => t("default_duration_no_options")}
options={selectedMultipleDuration}
onChange={(option) => {
@@ -388,32 +451,36 @@ export const EventSetupTab = (
<TextField
required
type="number"
{...lengthLockedProps}
label={t("duration")}
defaultValue={eventType.length ?? 15}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
/>
)}
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
}
}}
/>
</div>
{!lengthLockedProps.disabled && (
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
}
}}
/>
</div>
)}
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("location")}
{shouldLockIndicator("locations")}
</Skeleton>
<Controller
+89 -6
View File
@@ -1,4 +1,4 @@
import type { SchedulingType } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
@@ -7,24 +7,46 @@ import type { Options } from "react-select";
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Select } from "@calcom/ui";
interface IMemberToValue {
interface IUserToValue {
id: number | null;
name: string | null;
username: string | null;
email: string;
}
const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
value: `${id || ""}`,
label: `${name || ""}`,
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
email,
});
export const mapMemberToChildrenOption = (
member: EventTypeSetupProps["teamMembers"][number],
slug: string
) => {
return {
slug,
hidden: false,
created: false,
owner: {
id: member.id,
name: member.name ?? "",
email: member.email,
username: member.username ?? "",
membership: member.membership,
eventTypeSlugs: member.eventTypes ?? [],
},
value: `${member.id ?? ""}`,
label: member.name ?? "",
};
};
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
if (a.label < b.label) {
return -1;
@@ -35,6 +57,40 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
return 0;
};
const ChildrenEventTypesList = ({
options = [],
value,
onChange,
...rest
}: {
value: ReturnType<typeof mapMemberToChildrenOption>[];
onChange?: (options: ReturnType<typeof mapMemberToChildrenOption>[]) => void;
options?: Options<ReturnType<typeof mapMemberToChildrenOption>>;
} & Omit<Partial<ComponentProps<typeof ChildrenEventTypeSelect>>, "onChange" | "value">) => {
const { t } = useLocale();
return (
<div className="flex flex-col space-y-5">
<div>
<Label>{t("assign_to")}</Label>
<ChildrenEventTypeSelect
onChange={(options) => {
onChange &&
onChange(
options.map((option) => ({
...option,
}))
);
}}
value={value}
options={options.filter((opt) => !value.find((val) => val.owner.id.toString() === opt.value))}
controlShouldRenderValue={false}
{...rest}
/>
</div>
</div>
);
};
const CheckedHostField = ({
labelText,
placeholder,
@@ -52,7 +108,7 @@ const CheckedHostField = ({
options?: Options<CheckedSelectOption>;
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
return (
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div className="bg-muted flex flex-col space-y-5 p-4">
<div>
<Label>{labelText}</Label>
<CheckedTeamSelect
@@ -123,6 +179,21 @@ const RoundRobinHosts = ({
);
};
const ChildrenEventTypes = ({
childrenEventTypeOptions,
}: {
childrenEventTypeOptions: ReturnType<typeof mapMemberToChildrenOption>[];
}) => {
return (
<Controller<FormValues>
name="children"
render={({ field: { onChange, value } }) => (
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
)}
/>
);
};
const Hosts = ({
teamMembers,
}: {
@@ -189,6 +260,7 @@ const Hosts = ({
/>*/}
</>
),
MANAGED: <></>,
};
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
}}
@@ -196,7 +268,11 @@ const Hosts = ({
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
export const EventTeamTab = ({
team,
teamMembers,
eventType,
}: Pick<EventTypeSetupProps, "teamMembers" | "team" | "eventType">) => {
const { t } = useLocale();
const schedulingTypeOptions: {
@@ -216,9 +292,13 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
},
];
const teamMembersOptions = teamMembers.map(mapUserToValue);
const childrenEventTypeOptions = teamMembers.map((member) => {
return mapMemberToChildrenOption(member, eventType.slug);
});
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
return (
<div>
{team && (
{team && !isManagedEventType && (
<div className="space-y-5">
<div className="flex flex-col">
<Label>{t("scheduling_type")}</Label>
@@ -239,6 +319,9 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
<Hosts teamMembers={teamMembersOptions} />
</div>
)}
{team && isManagedEventType && (
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
)}
</div>
);
};
@@ -1,16 +1,17 @@
import type { Webhook } from "@prisma/client";
import { Webhook as TbWebhook } from "lucide-react";
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import { useState } from "react";
import { TbWebhook } from "react-icons/tb";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { WebhookForm } from "@calcom/features/webhooks/components";
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Webhook } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
import { FiPlus } from "@calcom/ui/components/icon";
import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
import { Plus, Lock } from "@calcom/ui/components/icon";
export const EventTeamWebhooksTab = ({
eventType,
@@ -85,12 +86,20 @@ export const EventTeamWebhooksTab = ({
<Button
color="secondary"
data-testid="new_webhook"
StartIcon={FiPlus}
StartIcon={Plus}
onClick={() => setCreateModalOpen(true)}>
{t("new_webhook")}
</Button>
);
};
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const webhookLockedStatus = shouldLockDisableProps("webhooks");
return (
<div>
{team && webhooks && !isLoading && (
@@ -98,15 +107,24 @@ export const EventTeamWebhooksTab = ({
<div>
<div>
<>
{isManagedEventType && (
<Alert
severity="neutral"
className="mb-2"
title={t("locked_for_members")}
message={t("locked_webhooks_description")}
/>
)}
{webhooks.length ? (
<>
<div className="mb-8 rounded-md border">
<div className="mb-2 rounded-md border">
{webhooks.map((webhook, index) => {
return (
<WebhookListItem
key={webhook.id}
webhook={webhook}
lastItem={webhooks.length === index + 1}
canEditWebhook={!webhookLockedStatus.disabled}
onEditWebhook={() => {
setEditModalOpen(true);
setWebhookToEdit(webhook);
@@ -122,7 +140,15 @@ export const EventTeamWebhooksTab = ({
Icon={TbWebhook}
headline={t("create_your_first_webhook")}
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
buttonRaw={<NewWebhookButton />}
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
<NewWebhookButton />
)
}
/>
)}
</>
@@ -1,10 +1,13 @@
import { SchedulingType } from "@prisma/client";
import { Webhook as TbWebhook } from "lucide-react";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import { useRouter } from "next/router";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form";
import { TbWebhook } from "react-icons/tb";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
@@ -32,19 +35,19 @@ import {
VerticalTabs,
} from "@calcom/ui";
import {
FiLink,
FiCalendar,
FiClock,
FiSliders,
FiRepeat,
FiGrid,
FiZap,
FiUsers,
FiExternalLink,
FiCode,
FiTrash,
FiMoreHorizontal,
FiLoader,
Link as LinkIcon,
Calendar,
Clock,
Sliders,
Repeat,
Grid,
Zap,
Users,
ExternalLink,
Code,
Trash,
MoreHorizontal,
Loader,
} from "@calcom/ui/components/icon";
import { EmbedButton, EmbedDialog } from "@components/Embed";
@@ -77,44 +80,38 @@ function getNavigation(props: {
{
name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`,
icon: FiLink,
icon: LinkIcon,
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
icon: FiCalendar,
info: `default_schedule_name`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${eventType.id}?tabName=limits`,
icon: FiClock,
icon: Clock,
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${eventType.id}?tabName=advanced`,
icon: FiSliders,
icon: Sliders,
info: `event_advanced_tab_description`,
},
{
name: "recurring",
href: `/event-types/${eventType.id}?tabName=recurring`,
icon: FiRepeat,
icon: Repeat,
info: `recurring_event_tab_description`,
},
{
name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`,
icon: FiGrid,
icon: Grid,
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`,
icon: FiZap,
icon: Zap,
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
];
@@ -137,7 +134,10 @@ function EventTypeSingleLayout({
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const hasPermsToDelete = currentUserMembership?.role !== "MEMBER" || !currentUserMembership;
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
@@ -157,23 +157,55 @@ function EventTypeSingleLayout({
},
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({
let navigation = getNavigation({
t,
eventType,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
});
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
icon: Calendar,
info:
isManagedEventType || isChildrenManagedEventType
? eventType.schedule === null
? "Member's default schedule"
: isChildrenManagedEventType
? `${
eventType.scheduleName
? `${eventType.scheduleName} - ${t("managed")}`
: `default_schedule_name`
}`
: eventType.scheduleName ?? `default_schedule_name`
: `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`,
icon: FiUsers,
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
icon: Users,
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
isManagedEventType
? ` - ${t("count_members", { count: formMethods.watch("children").length || 0 })}`
: ""
}`,
});
}
if (isManagedEventType || isChildrenManagedEventType) {
// Removing apps and workflows for manageg event types by admins v1
navigation = navigation.slice(0, -2);
} else {
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
@@ -189,6 +221,7 @@ function EventTypeSingleLayout({
}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
return (
<Shell
@@ -197,86 +230,96 @@ function EventTypeSingleLayout({
heading={eventType.title}
CTA={
<div className="flex items-center justify-end">
<div className="hidden items-center rounded-md px-2 sm:hover:bg-gray-100 lg:flex">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
{t("hide_from_profile")}
</Skeleton>
<Switch
id="hiddenSwitch"
checked={formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", e);
}}
/>
</div>
<VerticalDivider className="hidden lg:block" />
{!eventType.metadata.managedEventConfig && (
<>
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
{t("hide_from_profile")}
</Skeleton>
<Switch
id="hiddenSwitch"
checked={formMethods.watch("hidden")}
onCheckedChange={(e) => {
formMethods.setValue("hidden", e);
}}
/>
</div>
<VerticalDivider className="hidden lg:block" />
</>
)}
{/* TODO: Figure out why combined isnt working - works in storybook */}
<ButtonGroup combined containerProps={{ className: "border-gray-300 hidden lg:flex" }}>
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
<Tooltip content={t("preview")}>
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon={FiExternalLink}
/>
</Tooltip>
<ButtonGroup combined containerProps={{ className: "border-default hidden lg:flex" }}>
{!isManagedEventType && (
<>
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
<Tooltip content={t("preview")}>
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon={ExternalLink}
/>
</Tooltip>
<Button
color="secondary"
variant="icon"
StartIcon={FiLink}
tooltip={t("copy_link")}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}
/>
<EmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={FiCode}
color="secondary"
variant="icon"
tooltip={t("embed")}
/>
<Button
color="destructive"
variant="icon"
StartIcon={FiTrash}
tooltip={t("delete")}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}
/>
<Button
color="secondary"
variant="icon"
StartIcon={LinkIcon}
tooltip={t("copy_link")}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}
/>
<EmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon={Code}
color="secondary"
variant="icon"
tooltip={t("embed")}
/>
</>
)}
{!isChildrenManagedEventType && (
<Button
color="destructive"
variant="icon"
StartIcon={Trash}
tooltip={t("delete")}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}
/>
)}
</ButtonGroup>
<VerticalDivider className="hidden lg:block" />
<Dropdown>
<DropdownMenuTrigger asChild>
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
<Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent style={{ minWidth: "200px" }}>
<DropdownMenuItem className="focus:ring-gray-100">
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
target="_blank"
type="button"
StartIcon={FiExternalLink}
StartIcon={ExternalLink}
href={permalink}
rel="noreferrer">
{t("preview")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100">
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
type="button"
StartIcon={FiLink}
StartIcon={LinkIcon}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
@@ -284,18 +327,18 @@ function EventTypeSingleLayout({
{t("copy_link")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100">
<DropdownMenuItem className="focus:ring-muted">
<DropdownItem
type="button"
color="destructive"
StartIcon={FiTrash}
StartIcon={Trash}
disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="flex h-9 flex-row items-center justify-between py-2 px-4 sm:hover:bg-gray-100">
<div className="sm:hover:bg-subtle flex h-9 flex-row items-center justify-between py-2 px-4">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
@@ -312,7 +355,7 @@ function EventTypeSingleLayout({
</div>
</DropdownMenuContent>
</Dropdown>
<div className="border-l-2 border-gray-300" />
<div className="border-default border-l-2" />
<Button
className="ml-4 lg:ml-0"
type="submit"
@@ -323,7 +366,7 @@ function EventTypeSingleLayout({
</Button>
</div>
}>
<Suspense fallback={<FiLoader />}>
<Suspense fallback={<Loader />}>
<div className="flex flex-col xl:flex-row xl:space-x-6">
<div className="hidden xl:block">
<VerticalTabs
@@ -339,7 +382,7 @@ function EventTypeSingleLayout({
<div className="w-full ltr:mr-2 rtl:ml-2">
<div
className={classNames(
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
"bg-default border-subtle mt-4 rounded-md sm:mx-0 xl:mt-0",
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
)}>
{children}
@@ -351,14 +394,25 @@ function EventTypeSingleLayout({
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_event_type")}
confirmBtnText={t("confirm_delete_event_type")}
loadingText={t("confirm_delete_event_type")}
title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: eventType.id });
}}>
{t("delete_event_type_description")}
<p className="mt-5">
<Trans
i18nKey={`delete${isManagedEvent}_event_type_description`}
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
<ul>
<li>Members assigned to this event type will also have their event types deleted.</li>
<li>
Anyone who they&apos;ve shared their link with will no longer be able to book using it.
</li>
</ul>
</Trans>
</p>
</ConfirmationDialogContent>
</Dialog>
<EmbedDialog />
@@ -1,23 +1,26 @@
import type { FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Frequency } from "@calcom/prisma/zod-utils";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Alert, Select, SettingsToggle } from "@calcom/ui";
import { Alert, Select, SettingsToggle, TextField } from "@calcom/ui";
type RecurringEventControllerProps = {
recurringEvent: RecurringEvent | null;
eventType: EventTypeSetup;
paymentEnabled: boolean;
};
export default function RecurringEventController({
recurringEvent,
eventType,
paymentEnabled,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
eventType.recurringEvent
);
const formMethods = useFormContext<FormValues>();
/* Just yearly-0, monthly-1 and weekly-2 */
@@ -28,6 +31,14 @@ export default function RecurringEventController({
value: value.toString(),
}));
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const recurringLocked = shouldLockDisableProps("recurringEvent");
return (
<div className="block items-start sm:flex">
<div className={!paymentEnabled ? "w-full" : ""}>
@@ -37,6 +48,7 @@ export default function RecurringEventController({
<>
<SettingsToggle
title={t("recurring_event")}
{...recurringLocked}
description={t("recurring_event_description")}
checked={recurringEventState !== null}
data-testid="recurring-event-check"
@@ -45,7 +57,7 @@ export default function RecurringEventController({
formMethods.setValue("recurringEvent", null);
setRecurringEventState(null);
} else {
const newVal = recurringEvent || {
const newVal = eventType.recurringEvent || {
interval: 1,
count: 12,
freq: Frequency.WEEKLY,
@@ -57,12 +69,13 @@ export default function RecurringEventController({
{recurringEventState && (
<div data-testid="recurring-event-collapsible" className="text-sm">
<div className="flex items-center">
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
<input
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
<TextField
disabled={recurringLocked.disabled}
type="number"
min="1"
max="20"
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
className="mb-0"
defaultValue={recurringEventState.interval}
onChange={(event) => {
const newVal = {
@@ -77,7 +90,8 @@ export default function RecurringEventController({
options={recurringEventFreqOptions}
value={recurringEventFreqOptions[recurringEventState.freq]}
isSearchable={false}
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
className="w-18 ml-2 block min-w-0 rounded-md text-sm"
isDisabled={recurringLocked.disabled}
onChange={(event) => {
const newVal = {
...recurringEventState,
@@ -89,13 +103,14 @@ export default function RecurringEventController({
/>
</div>
<div className="mt-4 flex items-center">
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
<input
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
<TextField
disabled={recurringLocked.disabled}
type="number"
min="1"
max="20"
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
defaultValue={recurringEventState.count}
className="mb-0"
onChange={(event) => {
const newVal = {
...recurringEventState,
@@ -105,7 +120,7 @@ export default function RecurringEventController({
setRecurringEventState(newVal);
}}
/>
<p className="text-gray-900 ltr:mr-2 rtl:ml-2">
<p className="text-emphasis ltr:ml-2 rtl:mr-2">
{t("events", {
count: recurringEventState.count,
})}
@@ -1,25 +1,29 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import type { UnitTypeLongPlural } from "dayjs";
import { Trans } from "next-i18next";
import type { FormValues } from "pages/event-types/[type]";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type z from "zod";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Input, Label, SettingsToggle } from "@calcom/ui";
import { Input, SettingsToggle, RadioField, Select } from "@calcom/ui";
type RequiresConfirmationControllerProps = {
metadata: z.infer<typeof EventTypeMetaDataSchema>;
requiresConfirmation: boolean;
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
seatsEnabled: boolean;
eventType: EventTypeSetup;
};
export default function RequiresConfirmationController({
metadata,
eventType,
requiresConfirmation,
onRequiresConfirmation,
seatsEnabled,
@@ -37,6 +41,23 @@ export default function RequiresConfirmationController({
}
}, [requiresConfirmation]);
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
const options = [
{ label: t("minute_timeUnit"), value: "minutes" },
{ label: t("hour_timeUnit"), value: "hours" },
];
const defaultValue = options.find(
(opt) =>
opt.value === (metadata?.requiresConfirmationThreshold?.unit ?? defaultRequiresConfirmationSetup.unit)
);
return (
<div className="block items-start sm:flex">
<div className="w-full">
@@ -46,10 +67,11 @@ export default function RequiresConfirmationController({
render={() => (
<SettingsToggle
title={t("requires_confirmation")}
disabled={seatsEnabled}
disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
description={t("requires_confirmation_description")}
checked={requiresConfirmation}
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val);
onRequiresConfirmation(val);
@@ -77,73 +99,82 @@ export default function RequiresConfirmationController({
);
}
}}>
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
<div className="flex items-center">
<RadioGroup.Item
<div className="flex flex-col flex-wrap justify-start gap-y-2">
{(requiresConfirmationSetup === undefined || !requiresConfirmationLockedProps.disabled) && (
<RadioField
label={t("always_requires_confirmation")}
disabled={requiresConfirmationLockedProps.disabled}
id="always"
value="always"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="always" className="!m-0 flex items-center">
{t("always_requires_confirmation")}
</Label>
</div>
<div className="flex items-center">
<RadioGroup.Item
/>
)}
{(requiresConfirmationSetup !== undefined || !requiresConfirmationLockedProps.disabled) && (
<RadioField
disabled={requiresConfirmationLockedProps.disabled}
className="items-center"
label={
<>
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 inline-flex">
<Input
type="number"
min={1}
disabled={requiresConfirmationLockedProps.disabled}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.time",
val
);
}}
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<label
className={classNames(
requiresConfirmationLockedProps.disabled && "cursor-not-allowed"
)}>
<Select
inputId="notice"
options={options}
isSearchable={false}
isDisabled={requiresConfirmationLockedProps.disabled}
className="ml-2"
onChange={(opt) => {
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: opt?.value as UnitTypeLongPlural,
});
formMethods.setValue(
"metadata.requiresConfirmationThreshold.unit",
opt?.value as UnitTypeLongPlural
);
}}
defaultValue={defaultValue}
/>
</label>
</div>
),
}}
/>
</>
}
id="notice"
value="notice"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="notice" className="!m-0 flex items-center">
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 flex">
<Input
type="number"
min={1}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
}}
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<select
onChange={(evt) => {
const val = evt.target.value as UnitTypeLongPlural;
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
}}
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
defaultValue={
metadata?.requiresConfirmationThreshold?.unit ||
defaultRequiresConfirmationSetup.unit
}>
<option value="minutes">{t("minute_timeUnit")}</option>
<option value="hours">{t("hour_timeUnit")}</option>
</select>
</div>
),
}}
/>
</Label>
</div>
/>
)}
</div>
</RadioGroup.Root>
</SettingsToggle>
@@ -1,5 +1,5 @@
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
import { FiClock, FiUser } from "@calcom/ui/components/icon";
import { Clock, User } from "@calcom/ui/components/icon";
function SkeletonLoader() {
return (
@@ -11,7 +11,7 @@ function SkeletonLoader() {
<SkeletonText className="h-4 w-24" />
</div>
</div>
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<ul className="border-subtle bg-default divide-subtle divide-y rounded-md border sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
@@ -32,11 +32,11 @@ function SkeletonItem() {
<div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap">
<FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<Clock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
<SkeletonText className="h-4 w-12" />
</li>
<li className="flex items-center whitespace-nowrap">
<FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<User className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
<SkeletonText className="h-4 w-12" />
</li>
</ul>
@@ -6,17 +6,17 @@ import { Button } from "@calcom/ui";
interface ICalendarItem {
title: string;
description?: string;
imageSrc: string;
logo: string;
type: App["type"];
}
const CalendarItem = (props: ICalendarItem) => {
const { title, imageSrc, type } = props;
const { title, logo, type } = props;
const { t } = useLocale();
return (
<div className="flex flex-row items-center justify-between p-5">
<div className="flex items-center space-x-3">
<img src={imageSrc} alt={title} className="h-8 w-8" />
<img src={logo} alt={title} className="h-8 w-8" />
<p className="text-sm font-bold">{title}</p>
</div>
<InstallAppButtonWithoutPlanCheck
@@ -28,14 +28,14 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
<p className="font-sans text-sm font-bold leading-5">
{name}
{/* Temporarily removed till we use it on another place */}
{/* <span className="mx-1 rounded-[4px] bg-green-100 py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
{/* <span className="mx-1 rounded-[4px] bg-success py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
{t("default")}
</span> */}
</p>
<div className="fle-row flex">
<span
title={externalId}
className="max-w-44 mt-1 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm text-gray-500">
className="max-w-44 text-subtle mt-1 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm">
{externalId}{" "}
</span>
</div>
@@ -44,11 +44,11 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
{/* <Button
color="minimal"
type="button"
className="ml-auto flex rounded-md border border-gray-200 py-[10x] px-4 font-sans text-sm">
className="ml-auto flex rounded-md border border-subtle py-[10x] px-4 font-sans text-sm">
{t("edit")}
</Button> */}
</div>
<div className="h-[1px] w-full border-b border-gray-200" />
<div className="border-subtle h-[1px] w-full border-b" />
<div>
<ul className="p-4">
{calendars?.map((calendar, i) => (
@@ -16,7 +16,7 @@ const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps)
<>
<div className="mt-6 flex flex-row">
<div className="w-full">
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-gray-700">
<label htmlFor="createEventsOn" className="text-default flex text-sm font-medium">
{t("create_events_on")}
</label>
<div className="mt-2">
@@ -31,13 +31,13 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
firstCalendar &&
firstCalendar.integration &&
firstCalendar.integration.title &&
firstCalendar.integration.imageSrc && (
firstCalendar.integration.logo && (
<>
<List className="rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black">
<ConnectedCalendarItem
key={firstCalendar.integration.title}
name={firstCalendar.integration.title}
logo={firstCalendar.integration.imageSrc}
logo={firstCalendar.integration.logo}
externalId={
firstCalendar && firstCalendar.calendars && firstCalendar.calendars.length > 0
? firstCalendar.calendars[0].externalId
@@ -49,22 +49,22 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List>
{/* Create event on selected calendar */}
<CreateEventsOnCalendarSelect calendar={destinationCalendar} />
<p className="mt-4 text-sm text-gray-500">{t("connect_calendars_from_app_store")}</p>
<p className="text-subtle mt-4 text-sm">{t("connect_calendars_from_app_store")}</p>
</>
)}
{/* Connect calendars list */}
{firstCalendar === undefined && queryIntegrations.data && queryIntegrations.data.items.length > 0 && (
<List className="mx-1 divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black sm:mx-0">
<List className="bg-default divide-subtle border-subtle mx-1 divide-y rounded-md border p-0 dark:bg-black sm:mx-0">
{queryIntegrations.data &&
queryIntegrations.data.items.map((item) => (
<li key={item.title}>
{item.title && item.imageSrc && (
{item.title && item.logo && (
<CalendarItem
type={item.type}
title={item.title}
description={item.description}
imageSrc={item.imageSrc}
logo={item.logo}
/>
)}
</li>
@@ -73,7 +73,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
)}
{queryIntegrations.isLoading && (
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
{[0, 0, 0, 0].map((_item, index) => {
return (
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
@@ -89,7 +89,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
type="button"
data-testid="save-calendar-button"
className={classNames(
"mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm text-white",
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
disabledNextButton ? "cursor-not-allowed opacity-20" : ""
)}
onClick={() => nextStep()}
@@ -50,7 +50,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
return (
<Form
className="w-full bg-white text-black dark:bg-opacity-5 dark:text-white"
className="bg-default dark:text-inverted text-emphasis w-full dark:bg-opacity-5"
form={availabilityForm}
handleSubmit={async (values) => {
try {
@@ -117,7 +117,7 @@ const UserProfile = (props: IUserProfileProps) => {
name="avatar"
id="avatar"
placeholder="URL"
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"
className="border-default focus:ring-empthasis mt-1 block w-full rounded-sm border px-3 py-2 text-sm focus:border-gray-800 focus:outline-none"
defaultValue={imageSrc}
/>
<div className="flex items-center px-4">
@@ -142,19 +142,19 @@ const UserProfile = (props: IUserProfileProps) => {
</div>
</div>
<fieldset className="mt-8">
<Label className="mb-2 block text-sm font-medium text-gray-700">{t("about")}</Label>
<Label className="text-default mb-2 block text-sm font-medium">{t("about")}</Label>
<Editor
getText={() => md.render(getValues("bio") || user?.bio || "")}
setText={(value: string) => setValue("bio", turndown(value))}
excludedToolbarItems={["blockType"]}
/>
<p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white">
<p className="dark:text-inverted text-default mt-2 font-sans text-sm font-normal">
{t("few_sentences_about_yourself")}
</p>
</fieldset>
<Button
type="submit"
className="mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm text-white">
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
{t("finish")}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>
@@ -73,7 +73,7 @@ const UserSettings = (props: IUserSettingsProps) => {
{/* Full name textfield */}
<div className="w-full">
<label htmlFor="name" className="mb-2 block text-sm font-medium text-gray-700">
<label htmlFor="name" className="text-default mb-2 block text-sm font-medium">
{t("full_name")}
</label>
<input
@@ -85,7 +85,7 @@ const UserSettings = (props: IUserSettingsProps) => {
type="text"
autoComplete="off"
autoCorrect="off"
className="w-full rounded-md border border-gray-300 text-sm"
className="border-default w-full rounded-md border text-sm"
/>
{errors.name && (
<p data-testid="required" className="py-2 text-xs text-red-500">
@@ -95,7 +95,7 @@ const UserSettings = (props: IUserSettingsProps) => {
</div>
{/* Timezone select field */}
<div className="w-full">
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
<label htmlFor="timeZone" className="text-default block text-sm font-medium">
{t("timezone")}
</label>
@@ -106,7 +106,7 @@ const UserSettings = (props: IUserSettingsProps) => {
className="mt-2 w-full rounded-md text-sm"
/>
<p className="mt-3 flex flex-row font-sans text-xs leading-tight text-gray-500 dark:text-white">
<p className="text-subtle dark:text-inverted mt-3 flex flex-row font-sans text-xs leading-tight">
{t("current_time")} {dayjs().tz(selectedTimeZone).format("LT").toString().toLowerCase()}
</p>
</div>
+4 -2
View File
@@ -24,7 +24,9 @@ export default function WizardLayout({
}, [router.asPath]);
return (
<div className="dark:bg-brand dark:text-brand-contrast min-h-screen text-black" data-testid="onboarding">
<div
className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen"
data-testid="onboarding">
<div>
<Toaster position="bottom-right" />
</div>
@@ -34,7 +36,7 @@ export default function WizardLayout({
<div className="mx-auto sm:max-w-[520px]">
<header>
<p className="font-cal mb-3 text-[28px] font-medium leading-7">{title}&nbsp;</p>
<p className="font-sans text-sm font-normal text-gray-500">{subtitle}&nbsp;</p>
<p className="text-subtle font-sans text-sm font-normal">{subtitle}&nbsp;</p>
</header>
<Steps maxSteps={maxSteps} currentStep={currentStep} navigateToStep={noop} />
</div>
@@ -59,14 +59,14 @@ const ChangePasswordSection = () => {
return (
<>
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
<form className="divide-subtle divide-y lg:col-span-9" onSubmit={changePasswordHandler}>
<div className="py-6 lg:pb-5">
<div className="my-3">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("change_password")}</h2>
</div>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
<label htmlFor="current_password" className="text-default block text-sm font-medium">
{t("current_password")}
</label>
<div className="mt-1">
@@ -77,13 +77,13 @@ const ChangePasswordSection = () => {
name="current_password"
id="current_password"
required
className="block w-full rounded-sm border-gray-300 text-sm"
className="border-default block w-full rounded-sm text-sm"
placeholder={t("your_old_password")}
/>
</div>
</div>
<div className="w-full sm:w-1/2">
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
<label htmlFor="new_password" className="text-default block text-sm font-medium">
{t("new_password")}
</label>
<div className="mt-1">
@@ -94,7 +94,7 @@ const ChangePasswordSection = () => {
value={newPassword}
required
onInput={(e) => setNewPassword(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 text-sm"
className="border-default block w-full rounded-sm text-sm"
placeholder={t("super_secure_new_password")}
/>
</div>
@@ -70,10 +70,10 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
<div className="mb-4">
<PasswordField
labelProps={{
className: "block text-sm font-medium text-gray-700",
className: "block text-sm font-medium text-default",
}}
{...form.register("password")}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
<Label className="mt-4"> {t("2fa_code")}</Label>
@@ -82,7 +82,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>
@@ -22,14 +22,14 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
{t("user_impersonation_heading")}
</h2>
<Badge className="ml-2 text-xs" variant={!disableImpersonation ? "success" : "gray"}>
{!disableImpersonation ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="mt-1 text-sm text-gray-500">{t("user_impersonation_description")}</p>
<p className="text-subtle mt-1 text-sm">{t("user_impersonation_description")}</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Button
@@ -135,7 +135,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}>
<div className="mb-4">
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
<label htmlFor="password" className="text-default mt-4 block text-sm font-medium">
{t("password")}
</label>
<div className="mt-1">
@@ -146,7 +146,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
required
value={password}
onInput={(e) => setPassword(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 text-sm"
className="border-default block w-full rounded-sm text-sm"
/>
</div>
@@ -178,22 +178,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
className="ms-2 me-2"
onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button
type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
onClick={() => setStep(SetupStep.EnterTotpCode)}>
<Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
<Button type="submit" className="ms-2 me-2" disabled={isSubmitting}>
{t("enable")}
</Button>
</WithStep>
@@ -17,12 +17,12 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("2fa")}</h2>
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("2fa")}</h2>
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
{enabled ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
<p className="text-subtle mt-1 text-sm">{t("add_an_extra_layer_of_security")}</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Button
@@ -1,16 +1,16 @@
import { FiShield } from "@calcom/ui/components/icon";
import { Shield } from "@calcom/ui/components/icon";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<FiShield className="h-6 w-6 text-white" />
<Shield className="text-inverted h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
{title}
</h3>
<p className="text-sm text-gray-400">{description}</p>
<p className="text-muted text-sm">{description}</p>
</div>
</div>
);
@@ -3,7 +3,7 @@ import { useMutation } from "@tanstack/react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Badge, showToast, Switch } from "@calcom/ui";
import { FiArrowLeft } from "@calcom/ui/components/icon";
import { ArrowLeft } from "@calcom/ui/components/icon";
export function CalendarSwitch(props: {
type: string;
@@ -75,8 +75,8 @@ export function CalendarSwitch(props: {
/>
{props.defaultSelected && (
<Badge variant="gray">
<div className="flex">
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
<div className="flex items-center">
<ArrowLeft className="text-default mr-1 h-4 w-4" /> {t("adding_events_to")}
</div>
</Badge>
)}
@@ -77,10 +77,10 @@ const DisableTwoFactorAuthModal = ({
<div className="mb-4">
<PasswordField
labelProps={{
className: "block text-sm font-medium text-gray-700",
className: "block text-sm font-medium text-default",
}}
{...form.register("password")}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
/>
<Label className="mt-4"> {t("2fa_code")}</Label>
@@ -90,7 +90,7 @@ const DisableTwoFactorAuthModal = ({
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>
@@ -171,22 +171,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
className="ms-2 me-2"
onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button
type="submit"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
onClick={() => setStep(SetupStep.EnterTotpCode)}>
<Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
<Button type="submit" className="ms-2 me-2" disabled={isSubmitting}>
{t("enable")}
</Button>
</WithStep>
+4 -4
View File
@@ -9,7 +9,7 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
import { FiUserCheck } from "@calcom/ui/components/icon";
import { UserCheck } from "@calcom/ui/components/icon";
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
const { t } = useLocale();
@@ -24,7 +24,7 @@ export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser>
props.onSuccess();
}}>
<EmptyScreen
Icon={FiUserCheck}
Icon={UserCheck}
headline={t("admin_user_created")}
description={t("admin_user_created_description")}
/>
@@ -111,7 +111,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<Label htmlFor="username" className={classNames(longWebsiteUrl && "mb-0")}>
<span className="block">{t("username")}</span>
{longWebsiteUrl && (
<small className="items-centerpx-3 mt-2 inline-flex rounded-t-md border border-b-0 border-gray-300 bg-gray-100 py-1 px-3 text-gray-500">
<small className="items-centerpx-3 bg-subtle border-default text-subtle mt-2 inline-flex rounded-t-md border border-b-0 py-1 px-3">
{process.env.NEXT_PUBLIC_WEBSITE_URL}
</small>
)}
@@ -119,7 +119,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<TextField
addOnLeading={
!longWebsiteUrl && (
<span className="inline-flex items-center rounded-none px-3 text-sm text-gray-500">
<span className="text-subtle inline-flex items-center rounded-none px-3 text-sm">
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
</span>
)
+8 -8
View File
@@ -37,13 +37,13 @@ const ChooseLicense = (
<RadioGroup.Item value="FREE">
<div
className={classNames(
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
"bg-default cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
value === "FREE" && "ring-2 ring-black"
)}>
<h2 className="font-cal text-xl text-black">{t("agplv3_license")}</h2>
<h2 className="font-cal text-emphasis text-xl">{t("agplv3_license")}</h2>
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
<p className="text-gray-500">{t("forever_open_and_free")}</p>
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
<p className="text-subtle">{t("forever_open_and_free")}</p>
<ul className="text-subtle ml-4 list-disc text-left text-xs">
<li>{t("required_to_keep_your_code_open_source")}</li>
<li>{t("cannot_repackage_and_resell")}</li>
<li>{t("no_enterprise_features")}</li>
@@ -53,15 +53,15 @@ const ChooseLicense = (
<RadioGroup.Item value="EE">
<div
className={classNames(
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
"bg-default cursor-pointer space-y-2 rounded-md border p-4 hover:border-black",
value === "EE" && "ring-2 ring-black"
)}>
<h2 className="font-cal text-xl text-black">{t("ee_enterprise_license")}</h2>
<h2 className="font-cal text-emphasis text-xl">{t("ee_enterprise_license")}</h2>
<p className="font-medium text-green-800">
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
</p>
<p className="text-gray-500">{t("enterprise_license_includes")}</p>
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
<p className="text-subtle">{t("enterprise_license_includes")}</p>
<ul className="text-subtle ml-4 list-disc text-left text-xs">
<li>{t("no_need_to_keep_your_code_open_source")}</li>
<li>{t("repackage_rebrand_resell")}</li>
<li>{t("a_vast_suite_of_enterprise_features")}</li>
+10 -10
View File
@@ -10,7 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { Button, TextField } from "@calcom/ui";
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
import { Check, ExternalLink, Loader } from "@calcom/ui/components/icon";
type EnterpriseLicenseFormValues = {
licenseKey: string;
@@ -87,18 +87,18 @@ const EnterpriseLicense = (
return (
<FormProvider {...formMethods}>
<form {...rest} className="space-y-4 rounded-md bg-white px-8 py-10" onSubmit={handleSubmit}>
<form {...rest} className="bg-default space-y-4 rounded-md px-8 py-10" onSubmit={handleSubmit}>
<div>
<Button
className="w-full justify-center text-lg"
EndIcon={FiExternalLink}
EndIcon={ExternalLink}
href="https://console.cal.com"
target="_blank">
{t("purchase_license")}
</Button>
<div className="relative flex justify-center">
<hr className="my-8 w-full border-[1.5px] border-gray-200" />
<span className="absolute mt-[22px] bg-white px-3.5 text-sm">OR</span>
<hr className="border-subtle my-8 w-full border-[1.5px]" />
<span className="bg-default absolute mt-[22px] px-3.5 text-sm">OR</span>
</div>
{t("already_have_key")}
<Controller
@@ -108,7 +108,7 @@ const EnterpriseLicense = (
<TextField
{...formMethods.register("licenseKey")}
className={classNames(
"mb-0 group-hover:border-gray-400",
"group-hover:border-emphasis mb-0",
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
)}
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
@@ -116,14 +116,14 @@ const EnterpriseLicense = (
value={value}
addOnFilled={false}
addOnClassname={classNames(
"hover:border-gray-300",
errors.licenseKey === undefined && isDirty && "group-hover:border-gray-400"
"hover:border-default",
errors.licenseKey === undefined && isDirty && "group-hover:border-emphasis"
)}
addOnSuffix={
checkLicenseLoading ? (
<FiLoader className="h-5 w-5 animate-spin" />
<Loader className="h-5 w-5 animate-spin" />
) : errors.licenseKey === undefined && isDirty ? (
<FiCheck className="h-5 w-5 text-green-700" />
<Check className="h-5 w-5 text-green-700" />
) : undefined
}
color={errors.licenseKey ? "warn" : ""}
+3 -3
View File
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
import type { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { FiCheck } from "@calcom/ui/components/icon";
import { Check } from "@calcom/ui/components/icon";
const StepDone = (props: {
currentStep: number;
@@ -23,8 +23,8 @@ const StepDone = (props: {
router.replace(props.nextStepPath);
}}>
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
<div className="dark:bg-default flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600">
<Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
+6 -5
View File
@@ -2,6 +2,7 @@ import Link from "next/link";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { Avatar } from "@calcom/ui";
@@ -18,20 +19,20 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
return (
<Link key={member.id} href={`/${member.username}`}>
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
<Avatar
size="md"
alt={member.name || ""}
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
/>
<section className="line-clamp-4 mt-2 w-full space-y-1">
<p className="font-medium text-gray-900 dark:text-white">{member.name}</p>
<div className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white">
<p className="text-default font-medium">{member.name}</p>
<div className="line-clamp-3 text-subtle overflow-ellipsis text-sm font-normal">
{!isBioEmpty ? (
<>
<div
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: member.safeBio || "" }}
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
/>
</>
) : (
+4 -4
View File
@@ -24,18 +24,18 @@ 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")}>
{props.heading && <h2 className="font-cal text-center text-3xl text-gray-900">{props.heading}</h2>}
{props.heading && <h2 className="font-cal text-emphasis text-center text-3xl">{props.heading}</h2>}
</div>
{props.loading && (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
<div className="bg-muted absolute z-50 flex h-screen w-full items-center">
<Loader />
</div>
)}
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="mx-2 rounded-md border border-gray-200 bg-white px-4 py-10 sm:px-10">
<div className="bg-default border-subtle mx-2 rounded-md border px-4 py-10 sm:px-10">
{props.children}
</div>
<div className="mt-8 text-center text-sm text-gray-600">{props.footerText}</div>
<div className="text-default mt-8 text-center text-sm">{props.footerText}</div>
</div>
</div>
);
+3 -3
View File
@@ -2,7 +2,7 @@ import classNames from "classnames";
import { useState } from "react";
import type { ControllerRenderProps } from "react-hook-form";
import { FiEdit2 } from "@calcom/ui/components/icon";
import { Edit2 } from "@calcom/ui/components/icon";
const EditableHeading = function EditableHeading({
value,
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
<label className="min-w-8 relative inline-block">
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value}&nbsp;</span>
{!isEditing && isReady && (
<FiEdit2 className=" ml-1 -mt-px inline h-3 w-3 text-gray-500 group-hover:text-gray-500" />
<Edit2 className=" text-subtle group-hover:text-subtle ml-1 -mt-px inline h-3 w-3" />
)}
<input
{...passThroughProps}
@@ -29,7 +29,7 @@ const EditableHeading = function EditableHeading({
value={value}
required
className={classNames(
"absolute top-0 left-0 w-full cursor-pointer border-none bg-transparent p-0 align-top text-xl text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
"text-emphasis hover:text-default focus:text-emphasis absolute top-0 left-0 w-full cursor-pointer border-none bg-transparent p-0 align-top text-xl focus:outline-none focus:ring-0"
)}
onFocus={(e) => {
setIsEditing(true);
+2 -2
View File
@@ -1,12 +1,12 @@
import { Tooltip } from "@calcom/ui";
import { FiInfo } from "@calcom/ui/components/icon";
import { Info } from "@calcom/ui/components/icon";
export default function InfoBadge({ content }: { content: string }) {
return (
<>
<Tooltip side="top" content={content}>
<span title={content}>
<FiInfo className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" />
<Info className="text-subtle relative top-px left-1 right-1 mt-px h-4 w-4" />
</span>
</Tooltip>
</>
+2 -2
View File
@@ -12,8 +12,8 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
<button
type="button"
{...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">
<props.Icon className="h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
className="text-md hover:bg-emphasis hover:text-emphasis text-default flex items-center rounded-sm px-2 py-1 text-sm font-medium">
<props.Icon className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
{props.children}
</button>
</div>
+1 -1
View File
@@ -19,7 +19,7 @@ export default function ModalContainer(
<DialogContent>
<div
className={classNames(
"inline-block w-full transform bg-white text-left align-bottom transition-all sm:align-middle",
"bg-default inline-block w-full transform text-left align-bottom transition-all sm:align-middle",
{
"sm:w-full sm:max-w-lg ": !props.wide,
"sm:w-4xl sm:max-w-4xl": props.wide,
+1 -4
View File
@@ -9,10 +9,7 @@ const PoweredByCal = () => {
const isEmbed = useIsEmbed();
return (
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
<Link
href={POWERED_BY_URL}
target="_blank"
className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
<Link href={POWERED_BY_URL} target="_blank" className="text-subtle opacity-50 hover:opacity-100">
{t("powered_by")}{" "}
<img
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
@@ -13,8 +13,8 @@ export default function SettingInputContainer({
<div className="space-y-3">
<div className="block sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-gray-700">
<Icon className="mt-0.5 h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
<label htmlFor={htmlFor} className="text-default mt-1 flex text-sm font-medium">
<Icon className="text-subtle mt-0.5 h-4 w-4 ltr:mr-2 rtl:ml-2" />
{label}
</label>
</div>
@@ -14,8 +14,8 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
import { FiCheck, FiEdit2, FiExternalLink } from "@calcom/ui/components/icon";
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
export enum UsernameChangeStatusEnum {
UPGRADE = "UPGRADE",
@@ -195,7 +195,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<span
className={classNames(
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"
"border-default bg-muted text-subtle hidden h-9 items-center rounded-l-md border border-r-0 border-r-gray-300 px-3 text-sm md:inline-flex"
)}>
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
</span>
@@ -216,7 +216,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
: "border-l-gray-300",
disabled ? "bg-gray-100 text-gray-400 focus:border-0" : ""
disabled ? "bg-subtle text-muted focus:border-0" : ""
)}
value={inputUsernameValue}
onChange={(event) => {
@@ -235,7 +235,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
usernameIsAvailable ? "" : ""
)}>
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? <FiCheck className="mt-2 w-6" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 w-6" /> : <></>}
</span>
</div>
</div>
@@ -250,29 +250,30 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
<Dialog open={openDialogSaveUsername}>
<DialogContent>
<div className="flex flex-row">
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<FiEdit2 className="m-auto h-6 w-6" />
</div>
<div className="mb-4 w-full px-4 pt-1">
<DialogHeader title={t("confirm_username_change_dialog_title")} />
<DialogContent
Icon={Edit2}
title={t("confirm_username_change_dialog_title")}
description={
<>
{usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
<p className="mb-4 text-sm text-gray-800">{t("change_username_standard_to_premium")}</p>
<p className="text-default mb-4 text-sm">{t("change_username_standard_to_premium")}</p>
)}
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
</>
}>
<div className="flex flex-row">
<div className="mb-4 w-full px-4 pt-1">
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
<div className="flex-1 px-2">
<p className="text-gray-500">{t("current_username")}</p>
<p className="mt-1" data-testid="current-username">
<p className="text-subtle">{t("current_username")}</p>
<p className="text-emphasis mt-1" data-testid="current-username">
{currentUsername}
</p>
</div>
<div className="ml-6 flex-1">
<p className="text-gray-500" data-testid="new-username">
<p className="text-subtle" data-testid="new-username">
{t("new_username")}
</p>
<p>{inputUsernameValue}</p>
<p className="text-emphasis">{inputUsernameValue}</p>
</div>
</div>
</div>
@@ -287,7 +288,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
data-testid="go-to-billing"
href={paymentLink}>
<>
{t("go_to_stripe_billing")} <FiExternalLink className="ml-1 h-4 w-4" />
{t("go_to_stripe_billing")} <ExternalLink className="ml-1 h-4 w-4" />
</>
</Button>
)}
@@ -8,8 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui";
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
import { Button, Dialog, DialogClose, DialogContent, TextField } from "@calcom/ui";
import { Check, Edit2 } from "@calcom/ui/components/icon";
interface ICustomUsernameProps {
currentUsername: string | undefined;
@@ -79,7 +79,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
const ActionButtons = () => {
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
<div className="rlt:space-x-reverse mt-px flex flex-row space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
<div className="ms-2 me-2 mt-px flex flex-row space-x-2">
<Button
type="button"
onClick={() => setOpenDialogSaveUsername(true)}
@@ -123,7 +123,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
autoCapitalize="none"
autoCorrect="none"
className={classNames(
"mb-0 mt-0 rounded-md ltr:rounded-l-none rtl:rounded-r-none",
"mb-0 mt-0 rounded-md rounded-l-none",
markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
: ""
@@ -137,7 +137,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
{currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-6 flex flex-row">
<span className={classNames("mx-2 py-2")}>
{usernameIsAvailable ? <FiCheck className="w-6" /> : <></>}
{usernameIsAvailable ? <Check className="w-6" /> : <></>}
</span>
</div>
)}
@@ -154,31 +154,25 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
</div>
)}
<Dialog open={openDialogSaveUsername}>
<DialogContent>
<div style={{ display: "flex", flexDirection: "row" }}>
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<FiEdit2 className="m-auto h-6 w-6" />
</div>
<div className="mb-4 w-full px-4 pt-1">
<DialogHeader title={t("confirm_username_change_dialog_title")} />
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
<DialogContent type="confirmation" Icon={Edit2} title={t("confirm_username_change_dialog_title")}>
<div className="flex flex-row">
<div className="mb-4 w-full pt-1">
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
<div className="flex-1 px-2">
<p className="text-gray-500">{t("current_username")}</p>
<p className="mt-1" data-testid="current-username">
<p className="text-subtle">{t("current_username")}</p>
<p className="text-emphasis mt-1" data-testid="current-username">
{currentUsername}
</p>
</div>
<div className="flex-1">
<p className="text-gray-500" data-testid="new-username">
<div className="ml-6 flex-1">
<p className="text-subtle" data-testid="new-username">
{t("new_username")}
</p>
<p>{inputUsernameValue}</p>
<p className="text-emphasis">{inputUsernameValue}</p>
</div>
</div>
</div>
</div>
<div className="mt-4 flex flex-row-reverse gap-x-2">
<Button
type="button"
@@ -22,7 +22,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{React.createElement(
descriptionAsLabel ? "div" : "label",
{
className: "flex text-sm font-medium text-gray-700",
className: "flex text-sm font-medium text-default",
...(!descriptionAsLabel
? {
htmlFor: rest.id,
@@ -40,7 +40,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{
className: classNames(
"relative flex items-start",
descriptionAsLabel ? "text-gray-700" : "text-gray-900"
descriptionAsLabel ? "text-default" : "text-emphasis"
),
},
<>
@@ -49,10 +49,10 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{...rest}
ref={ref}
type="checkbox"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300"
className="text-primary-600 focus:ring-primary-500 border-default h-4 w-4 rounded"
/>
</div>
<span className="text-sm ltr:ml-3 rtl:mr-3">{description}</span>
<span className="ms-3 text-sm">{description}</span>
</>
)}
{informationIconText && <InfoBadge content={informationIconText} />}
@@ -3,7 +3,7 @@ import type { Props } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
import { X } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select";
@@ -49,9 +49,9 @@ export const CheckedSelect = ({
alt={option.label}
/>
{option.label}
<FiX
<X
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500"
className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
/>
</div>
))}
+3 -3
View File
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
import "react-date-picker/dist/DatePicker.css";
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
import { FiCalendar } from "@calcom/ui/components/icon";
import { Calendar } from "@calcom/ui/components/icon";
import classNames from "@lib/classNames";
@@ -19,11 +19,11 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
return (
<PrimitiveDatePicker
className={classNames(
"focus:ring-primary-500 focus:border-primary-500 rounded-sm border border-gray-300 p-1 pl-2 text-sm",
"focus:ring-primary-500 focus:border-primary-500 border-default rounded-sm border p-1 pl-2 text-sm",
className
)}
clearIcon={null}
calendarIcon={<FiCalendar className="h-5 w-5 text-gray-500" />}
calendarIcon={<Calendar className="text-subtle h-5 w-5" />}
value={date}
minDate={minDate}
disabled={disabled}
@@ -19,7 +19,10 @@ export type GroupOptionType = GroupBase<LocationOption>;
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
return (
<div className="flex items-center gap-3">
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5" />}
{/* TODO: figure out a way to invert icons when in dark mode. We can't just
dark:invert due to google meet cal etc all breaking when we do this
*/}
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5 dark:hidden" />}
<span className={classNames("text-sm font-medium")}>{label}</span>
</div>
);
@@ -48,7 +51,7 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
<span>{e.label}</span>
</div>
)}
formatGroupLabel={(e) => <p className="text-xs font-medium text-gray-600">{e.label}</p>}
formatGroupLabel={(e) => <p className="text-default text-xs font-medium">{e.label}</p>}
{...props}
/>
);
+3 -3
View File
@@ -11,7 +11,7 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
<div className="block sm:flex">
{!!label && (
<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-gray-700">
<label htmlFor={rest.id} className="text-default flex h-full items-center text-sm font-medium">
{label}
</label>
</div>
@@ -23,12 +23,12 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
ref={ref}
type="number"
className={classNames(
"block w-full rounded-sm border-gray-300 pl-2 pr-12 text-sm",
"border-default block w-full rounded-sm pl-2 pr-12 text-sm",
rest.className
)}
/>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<span className="text-sm text-gray-500" id="duration">
<span className="text-subtle text-sm" id="duration">
mins
</span>
</div>
+86 -19
View File
@@ -6,7 +6,8 @@ import { appWithTranslation } from "next-i18next";
import { ThemeProvider } from "next-themes";
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
import type { NextRouter } from "next/router";
import type { ComponentProps, ReactNode } from "react";
import { useRouter } from "next/router";
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
@@ -26,8 +27,10 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
Component: NextAppProps["Component"] & {
requiresLicense?: boolean;
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean);
isThemeSupported?: boolean;
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
PageWrapper?: (props: AppProps) => JSX.Element;
};
/** Will be defined only is there was an error */
@@ -60,6 +63,83 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
return <I18nextAdapter {...passedProps} />;
};
const enum ThemeSupport {
// e.g. Login Page
None = "none",
// Entire App except Booking Pages
App = "systemOnly",
// Booking Pages(including Routing Forms)
Booking = "userConfigured",
}
const CalcomThemeProvider = (
props: PropsWithChildren<
WithNonceProps & {
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
isThemeSupported?: boolean;
}
>
) => {
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
// undefined or true means we use system theme
const router = useRouter();
const isBookingPage = (() => {
if (typeof props.isBookingPage === "function") {
return props.isBookingPage({ router: router });
}
return props.isBookingPage;
})();
const themeSupport = isBookingPage
? ThemeSupport.Booking
: // if isThemeSupported is explicitly false, we don't use theme there
props.isThemeSupported === false
? ThemeSupport.None
: ThemeSupport.App;
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
const isEmbedMode = typeof embedNamespace === "string";
const storageKey = isEmbedMode
? `embed-theme-${embedNamespace}`
: themeSupport === ThemeSupport.App
? "app-theme"
: themeSupport === ThemeSupport.Booking
? "booking-theme"
: undefined;
return (
<ThemeProvider
nonce={props.nonce}
enableColorScheme={false}
enableSystem={themeSupport !== ThemeSupport.None}
forcedTheme={forcedTheme}
storageKey={storageKey}
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
// This is how login to dashboard soft navigation changes theme from light to dark
key={storageKey}
attribute="class">
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
{typeof window !== "undefined" && !isEmbedMode && (
<style jsx global>
{`
.dark {
color-scheme: dark;
}
`}
</style>
)}
{props.children}
</ThemeProvider>
);
};
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
const flags = useFlags();
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
@@ -69,33 +149,20 @@ const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.viewer.public.session.useQuery().data;
// No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage();
const isThemeSupported =
typeof props.Component.isThemeSupported === "function"
? props.Component.isThemeSupported({ router: props.router })
: props.Component.isThemeSupported;
const forcedTheme = isThemeSupported ? undefined : "light";
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
// One such example is our Embeds Demo and Testing page at http://localhost:3100
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
const storageKey = typeof embedNamespace === "string" ? `embed-theme-${embedNamespace}` : "theme";
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider session={session || undefined}>
<CustomI18nextProvider {...props}>
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<ThemeProvider
<CalcomThemeProvider
nonce={props.pageProps.nonce}
enableColorScheme={false}
storageKey={storageKey}
forcedTheme={forcedTheme}
attribute="class">
isThemeSupported={props.Component.isThemeSupported}
isBookingPage={props.Component.isBookingPage}>
<FeatureFlagsProvider>
<MetaProvider>{props.children}</MetaProvider>
</FeatureFlagsProvider>
</ThemeProvider>
</CalcomThemeProvider>
</TooltipProvider>
</CustomI18nextProvider>
</SessionProvider>
+1 -4
View File
@@ -13,9 +13,6 @@ function getCspPolicy(nonce: string) {
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
// Maybe see how @next-safe/middleware does it if it's supported.
const useNonStrictPolicy = CSP_POLICY === "non-strict";
const SENTRY_ENDPOINT = process.env.NEXT_PUBLIC_SENTRY_DSN
? new URL(process.env.NEXT_PUBLIC_SENTRY_DSN, "http://base_url").origin
: "";
// We add WEBAPP_URL to img-src because of booking pages, which end up loading images from app.cal.com on cal.com
// FIXME: Write a layer to extract out EventType Analytics tracking endpoints and add them to img-src or connect-src as needed. e.g. fathom, Google Analytics and others
@@ -36,7 +33,7 @@ function getCspPolicy(nonce: string) {
} app.cal.com;
font-src 'self';
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
connect-src 'self' ${SENTRY_ENDPOINT}
connect-src 'self'
`;
}
+3 -21
View File
@@ -1,6 +1,5 @@
require("dotenv").config({ path: "../../.env" });
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { withSentryConfig } = require("@sentry/nextjs");
const os = require("os");
const { withAxiom } = require("next-axiom");
@@ -92,11 +91,11 @@ const nextConfig = {
"@calcom/prisma",
"@calcom/trpc",
"@calcom/ui",
"lucide-react",
],
modularizeImports: {
"@calcom/ui/components/icon": {
transform: "@react-icons/all-files/fi/{{member}}",
skipDefaultConversion: true,
transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
preventFullImport: true,
},
"@calcom/features/insights/components": {
@@ -318,21 +317,4 @@ const nextConfig = {
},
};
const sentryWebpackPluginOptions = {
silent: true, // Suppresses all logs
};
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
nextConfig.sentry = {
hideSourceMaps: true,
// Prevents Sentry from running on this Edge function, where Sentry doesn't work yet (build whould crash the api route).
excludeServerRoutes: [/\/api\/social\/og\/image\/?/],
};
}
// Sentry should be the last thing to export to catch everything right
module.exports = process.env.NEXT_PUBLIC_SENTRY_DSN
? withSentryConfig(moduleExports, sentryWebpackPluginOptions)
: moduleExports;
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.7.8",
"version": "2.8.3",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -57,7 +57,6 @@
"@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-toggle-group": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0",
"@sentry/nextjs": "^7.20.0",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",
@@ -170,7 +169,7 @@
"module-alias": "^2.2.2",
"msw": "^0.42.3",
"postcss": "^8.4.18",
"tailwindcss": "^3.2.1",
"tailwindcss": "^3.3.1",
"ts-jest": "^28.0.8",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
+53 -47
View File
@@ -6,7 +6,9 @@ import { useEffect, useState } from "react";
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
import { BookOpen, Check, ChevronRight, FileText } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
import { ssgInit } from "@server/lib/ssg";
@@ -20,13 +22,13 @@ export default function Custom404() {
{
title: t("documentation"),
description: t("documentation_description"),
icon: FiFileText,
icon: FileText,
href: DOCS_URL,
},
{
title: t("blog"),
description: t("blog_description"),
icon: FiBookOpen,
icon: BookOpen,
href: `${WEBSITE_URL}/blog`,
},
];
@@ -88,21 +90,21 @@ export default function Custom404() {
noindex: true,
}}
/>
<div className="min-h-screen bg-white px-4" data-testid="404-page">
<div className="bg-default min-h-screen px-4" data-testid="404-page">
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
{isSignup && process.env.NEXT_PUBLIC_WEBAPP_URL !== "https://app.cal.com" ? (
<div>
<div>
<p className="text-sm font-semibold uppercase tracking-wide text-black">
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">
{t("missing_license")}
</p>
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
<h1 className="font-cal text-emphasis mt-2 text-3xl font-extrabold">
{t("signup_requires")}
</h1>
<p className="mt-4">{t("signup_requires_description", { companyName: "Cal.com" })}</p>
</div>
<div className="mt-12">
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">
{t("next_steps")}
</h2>
<ul role="list" className="mt-4">
@@ -112,48 +114,48 @@ export default function Custom404() {
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
<Check className="h-6 w-6 text-green-500" 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">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true" />
{t("acquire_commercial_license")}
</span>
</span>
</h3>
<p className="text-base text-gray-500">{t("the_infrastructure_plan")}</p>
<p className="text-subtle text-base">{t("the_infrastructure_plan")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
<ul role="list" className="divide-y divide-gray-200 border-gray-200">
<ul role="list" className="border-subtle divide-subtle divide-y">
<li className="px-4 py-2">
<Link
href="https://cal.com/self-hosting/installation"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
<FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<FileText className="text-default h-6 w-6" 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">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
{t("prisma_studio_tip")}
</span>
</h3>
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
<p className="text-subtle text-base">{t("prisma_studio_tip_description")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</Link>
</li>
@@ -162,7 +164,7 @@ export default function Custom404() {
href={JOIN_SLACK}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<svg
viewBox="0 0 2447.6 2452.5"
className="h-6 w-6"
@@ -189,24 +191,24 @@ export default function Custom404() {
</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">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
Slack
</span>
</h3>
<p className="text-base text-gray-500">{t("join_our_community")}</p>
<p className="text-subtle text-base">{t("join_our_community")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
<div className="mt-8">
<Link
href="https://cal.com/sales"
className="text-base font-medium text-black hover:text-gray-500">
href={`${WEBSITE_URL}/enterprise`}
className="hover:text-subtle text-emphasis text-base font-medium">
{t("contact_sales")}
<span aria-hidden="true"> &rarr;</span>
</Link>
@@ -216,8 +218,10 @@ export default function Custom404() {
) : (
<>
<div className="text-center">
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
<p className="text-emphasis text-sm font-semibold uppercase tracking-wide">
{t("error_404")}
</p>
<h1 className="font-cal text-emphasis mt-2 text-4xl font-extrabold sm:text-5xl">
{isSuccessPage ? "Booking not found" : t("page_doesnt_exist")}
</h1>
{isSubpage ? (
@@ -242,7 +246,7 @@ export default function Custom404() {
)}
</div>
<div className="mt-12">
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">
{t("popular_pages")}
</h2>
{!isSubpage && isCalcom && (
@@ -255,50 +259,50 @@ export default function Custom404() {
rel="noreferrer">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
<Check className="h-6 w-6 text-green-500" 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">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="focus:outline-none">
<span className="absolute inset-0" aria-hidden="true" />
{t("register")} <strong className="text-green-500">{username}</strong>
</span>
</span>
</h3>
<p className="text-base text-gray-500">{t("claim_username_and_schedule_events")}</p>
<p className="text-subtle text-base">{t("claim_username_and_schedule_events")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
)}
<ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200">
<ul role="list" className="border-subtle divide-subtle mt-4 divide-y">
{links.map((link, linkIdx) => (
<li key={linkIdx} className="px-4 py-2">
<Link
href={link.href}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="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 className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<link.icon className="text-default h-6 w-6" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
{link.title}
</span>
</h3>
<p className="text-base text-gray-500">{link.description}</p>
<p className="text-subtle text-base">{link.description}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</Link>
</li>
@@ -308,7 +312,7 @@ export default function Custom404() {
href={JOIN_SLACK}
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
<svg
viewBox="0 0 2447.6 2452.5"
className="h-6 w-6"
@@ -335,22 +339,22 @@ export default function Custom404() {
</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">
<h3 className="text-emphasis text-base font-medium">
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
Slack
</span>
</h3>
<p className="text-base text-gray-500">{t("join_our_community")}</p>
<p className="text-subtle text-base">{t("join_our_community")}</p>
</div>
<div className="flex-shrink-0 self-center">
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
</div>
</a>
</li>
</ul>
<div className="mt-8">
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
<Link href="/" className="hover:text-subtle text-emphasis text-base font-medium">
{t("or_go_back_home")}
<span aria-hidden="true"> &rarr;</span>
</Link>
@@ -364,6 +368,8 @@ export default function Custom404() {
);
}
Custom404.PageWrapper = PageWrapper;
export const getStaticProps = async (context: GetStaticPropsContext) => {
const ssr = await ssgInit(context);
+13 -9
View File
@@ -4,37 +4,39 @@ import { useRouter } from "next/router";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";
import { FiCopy } from "@calcom/ui/components/icon";
import { Copy } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
export default function Error500() {
const { t } = useLocale();
const router = useRouter();
return (
<div className="flex h-screen bg-gray-100">
<div className="bg-subtle flex h-screen">
<Head>
<title>Something unexpected occurred | {APP_NAME}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="rtl: m-auto rounded-md bg-white p-10 text-right ltr:text-left">
<h1 className="font-cal text-6xl text-black">500</h1>
<h2 className="mt-6 text-2xl font-medium text-black">It&apos;s not you, it&apos;s us.</h2>
<p className="mt-4 mb-6 max-w-2xl text-sm text-gray-600">
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
<h1 className="font-cal text-emphasis text-6xl">500</h1>
<h2 className="text-emphasis mt-6 text-2xl font-medium">It&apos;s not you, it&apos;s us.</h2>
<p className="text-default mt-4 mb-6 max-w-2xl text-sm">
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
away for you.
</p>
{router.query.error && (
<div className="mb-8 flex flex-col">
<p className="mb-4 max-w-2xl text-sm text-gray-600">
<p className="text-default mb-4 max-w-2xl text-sm">
Please provide the following text when contacting support to better help you:
</p>
<pre className="w-full max-w-2xl whitespace-normal break-words rounded-md bg-gray-200 p-4 text-gray-900">
<pre className="bg-emphasis text-emphasis w-full max-w-2xl whitespace-normal break-words rounded-md p-4">
{router.query.error}
<br />
<Button
color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon={FiCopy}
StartIcon={Copy}
onClick={() => {
navigator.clipboard.writeText(router.query.error as string);
showToast("Link copied!", "success");
@@ -52,3 +54,5 @@ export default function Error500() {
</div>
);
}
Error500.PageWrapper = PageWrapper;
+31 -44
View File
@@ -3,7 +3,6 @@ import classNames from "classnames";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { Toaster } from "react-hot-toast";
import {
@@ -14,7 +13,6 @@ import {
} from "@calcom/embed-core/embed-iframe";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import CustomBranding from "@calcom/lib/CustomBranding";
import { WEBAPP_URL } from "@calcom/lib/constants";
import defaultEvents, {
getDynamicEventDescription,
@@ -25,16 +23,17 @@ import defaultEvents, {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
import { FiArrowRight } from "@calcom/ui/components/icon";
import { ArrowRight } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import { ssrInit } from "@server/lib/ssr";
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
@@ -48,9 +47,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? (
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">{" " + t("unavailable")}</h2>
<div className="overflow-hidden rounded-sm border ">
<div className="text-muted p-8 text-center">
<h2 className="font-cal text-default mb-2 text-3xl">{" " + t("unavailable")}</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled") as string}</p>
</div>
</div>
@@ -60,14 +59,14 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{eventTypes.map((type, index) => (
<li
key={index}
className="dark:bg-darkgray-100 dark:border-darkgray-300 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">
<FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<ArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
<Link
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
className="flex justify-between px-6 py-4"
data-testid="event-type-link">
<div className="flex-shrink">
<p className="dark:text-darkgray-700 text-sm font-semibold text-gray-900">{type.title}</p>
<p className=" text-emphasis text-sm font-semibold">{type.title}</p>
<EventTypeDescription className="text-sm" eventType={type} />
</div>
<div className="mt-1 self-center">
@@ -94,14 +93,15 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
const query = { ...router.query };
delete query.user; // So it doesn't display in the Link (and make tests fail)
const nameOrUsername = user.name || user.username || "";
const telemetry = useTelemetry();
useEffect(() => {
/*
const telemetry = useTelemetry();
useEffect(() => {
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
}
}, [telemetry, router.asPath]);
}, [telemetry, router.asPath]); */
const isEventListEmpty = eventTypes.length === 0;
return (
<>
@@ -118,53 +118,39 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
: [{ username: `${user.username}`, name: `${user.name}` }],
}}
/>
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "max-w-3xl" : "",
"dark:bg-darkgray-50"
)}>
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed
? " border-bookinglightest dark:bg-darkgray-50 rounded-md border bg-white sm:dark:border-gray-600"
: "",
isEmbed ? " border-booker border-booker-width bg-default rounded-md border" : "",
"max-w-3xl py-24 px-4"
)}>
{isSingleUser && ( // When we deal with a single user, not dynamic group
<div className="mb-8 text-center">
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
<h1 className="font-cal mb-1 text-3xl text-gray-900 dark:text-white">
<h1 className="font-cal text-emphasis mb-1 text-3xl">
{nameOrUsername}
{user.verified && (
<BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
)}
{user.verified && <BadgeCheckIcon className=" mx-1 -mt-1 inline h-6 w-6 text-blue-500" />}
</h1>
{!isBioEmpty && (
<>
<div
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: props.safeBio }}
/>
</>
)}
</div>
)}
<div
className={classNames(
"rounded-md ",
!isEventListEmpty && "dark:border-darkgray-300 border border-gray-200"
)}
className={classNames("rounded-md ", !isEventListEmpty && "border-subtle border")}
data-testid="event-types">
{user.away ? (
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">
😴{" " + t("user_away")}
</h2>
<div className="overflow-hidden rounded-sm border ">
<div className="text-muted p-8 text-center">
<h2 className="font-cal text-default mb-2 text-3xl">😴{" " + t("user_away")}</h2>
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
</div>
</div>
@@ -175,8 +161,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<div
key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
className="dark:bg-darkgray-100 dark:hover:bg-darkgray-200 dark:border-darkgray-300 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">
<FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
<ArrowRight className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<div className="block w-full p-5">
<Link
@@ -193,17 +179,16 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
}}
data-testid="event-type-link">
<div className="flex flex-wrap items-center">
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
{type.title}
</h2>
<h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
</div>
<EventTypeDescription eventType={type} />
<EventTypeDescription eventType={type} isPublic={true} />
</Link>
</div>
</div>
))
)}
</div>
{isEventListEmpty && <EmptyPage name={user.name ?? "User"} />}
</main>
<Toaster position="bottom-right" />
@@ -211,7 +196,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
</>
);
}
User.isThemeSupported = true;
User.isBookingPage = true;
User.PageWrapper = PageWrapper;
const getEventTypesWithHiddenFromDB = async (userId: number) => {
return (
+8 -6
View File
@@ -12,6 +12,7 @@ import { isBrandingHidden } from "@lib/isBrandingHidden";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps> & EmbedProps;
@@ -20,12 +21,12 @@ export default function Type(props: AvailabilityPageProps) {
const { t } = useLocale();
return props.away ? (
<div className="h-screen dark:bg-gray-900">
<div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
😴{" " + t("user_away")}
</h2>
<p className="mx-auto max-w-md">{t("user_away_description")}</p>
@@ -39,8 +40,8 @@ export default function Type(props: AvailabilityPageProps) {
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
{" " + t("unavailable")}
</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled")}</p>
@@ -54,7 +55,8 @@ export default function Type(props: AvailabilityPageProps) {
);
}
Type.isThemeSupported = true;
Type.isBookingPage = true;
Type.PageWrapper = PageWrapper;
const paramsSchema = z.object({ type: z.string(), user: z.string() });
async function getUserPageProps(context: GetStaticPropsContext) {
+9 -7
View File
@@ -26,6 +26,7 @@ import {
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import BookingPage from "@components/booking/pages/BookingPage";
import { ssrInit } from "@server/lib/ssr";
@@ -35,12 +36,12 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) {
const { t } = useLocale();
return props.away ? (
<div className="h-screen dark:bg-gray-900">
<div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-default mb-2 text-3xl">
😴{" " + t("user_away")}
</h2>
<p className="mx-auto max-w-md">{t("user_away_description")}</p>
@@ -50,12 +51,12 @@ export default function Book(props: BookPageProps) {
</main>
</div>
) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
<div className="h-screen dark:bg-gray-900">
<div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-default mb-2 text-3xl">
{" " + t("unavailable")}
</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled")}</p>
@@ -69,7 +70,8 @@ export default function Book(props: BookPageProps) {
);
}
Book.isThemeSupported = true;
Book.isBookingPage = true;
Book.PageWrapper = PageWrapper;
const querySchema = z.object({
bookingUid: z.string().optional(),
@@ -47,7 +47,7 @@ export const getStaticProps: GetStaticProps<
revalidate: 1,
};
} catch (error) {
let message = "Unknown error while fetching calendarƒ";
let message = "Unknown error while fetching calendar";
if (error instanceof Error) message = error.message;
console.error(error, message);
return {
+3 -72
View File
@@ -1,82 +1,13 @@
import { DefaultSeo } from "next-seo";
import { Inter } from "next/font/google";
import localFont from "next/font/local";
import Head from "next/head";
import Script from "next/script";
import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { trpc } from "@calcom/trpc/react";
import type { AppProps } from "@lib/app-providers";
import AppProviders from "@lib/app-providers";
import { seoConfig } from "@lib/config/next-seo.config";
import I18nLanguageHandler from "@components/I18nLanguageHandler";
import "../styles/globals.css";
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
const calFont = localFont({
src: "../fonts/CalSans-SemiBold.woff2",
variable: "--font-cal",
preload: true,
display: "swap",
});
function MyApp(props: AppProps) {
const { Component, pageProps, err, router } = props;
let pageStatus = "200";
if (router.pathname === "/404") {
pageStatus = "404";
} else if (router.pathname === "/500") {
pageStatus = "500";
}
// On client side don't let nonce creep into DOM
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
// See https://github.com/kentcdodds/nonce-hydration-issues
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
const providerProps = {
...props,
pageProps: {
...props.pageProps,
nonce,
},
};
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
return (
<AppProviders {...providerProps}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<I18nLanguageHandler />
<Script
nonce={nonce}
id="page-status"
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
/>
<style jsx global>{`
:root {
--font-inter: ${interFont.style.fontFamily};
--font-cal: ${calFont.style.fontFamily};
}
`}</style>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
{getLayout(
Component.requiresLicense ? (
<LicenseRequired>
<Component {...pageProps} err={err} />
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
),
router
)}
</AppProviders>
);
const { Component, pageProps } = props;
if (Component.PageWrapper !== undefined) return Component.PageWrapper(props);
return <Component {...pageProps} />;
}
export default trpc.withTRPC(MyApp);
+2 -4
View File
@@ -1,6 +1,5 @@
import type { DocumentContext, DocumentProps } from "next/document";
import Document, { Head, Html, Main, NextScript } from "next/document";
import Script from "next/script";
import { z } from "zod";
import { getDirFromLang } from "@calcom/lib/i18n";
@@ -41,12 +40,11 @@ class MyDocument extends Document<Props> {
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
<meta name="msapplication-TileColor" content="#ff0000" />
<meta name="theme-color" content="#ffffff" />
<Script src="/embed-init-iframe.js" strategy="beforeInteractive" />
<meta name="theme-color" content="var(--cal-bg)" />
</Head>
<body
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100 antialiased"
className="dark:bg-darkgray-50 desktop-transparent bg-subtle antialiased"
style={
isEmbed
? {

Some files were not shown because too many files have changed in this diff Show More