Compare commits

..
183 Commits
Author SHA1 Message Date
zomars 77bf376c94 v1.8.0 2022-07-15 11:20:40 -06:00
zomars 200a704756 Submodule sync 2022-07-15 11:20:12 -06:00
6dd63ee3da Bug(header): profile dropdown flickering on mobile (#3071)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-07-15 11:17:54 -06:00
sean-brydonandOmar López e99b018d3b Handle fake video apps (#3232)
Co-authored-by: Omar López <zomars@me.com>
2022-07-15 19:13:31 +02:00
github-actions[bot]andCrowdin Bot 8f020bf1d0 New Crowdin translations by Github Action (#3385)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-15 11:00:37 -06:00
Peer Richelsenandkodiakhq[bot] 78e2db6d70 extracted upgrade to pro strings (#3383)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-15 10:55:59 -06:00
ff6a7bf2a6 Fixes broken avatar slug link in availability page : OG image (#3255)
* fixed broken avatar slug link

* alternate fix

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-15 10:54:14 -06:00
0433b0a69a New Crowdin translations by Github Action (#3382)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-07-15 18:40:39 +02:00
Peer RichelsenandOmar López ee552d8d64 purged shadow-sm (#3381)
Co-authored-by: Omar López <zomars@me.com>
2022-07-15 18:39:11 +02:00
Carina Wollendorfer dda5750bbc Add workflow setup information to README.md (#3379) 2022-07-15 09:38:09 -06:00
18389f459d minor improvements to routing forms (#3380)
* minor improvements to routing forms

* UX improvements and text changes

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-15 11:46:05 +00:00
github-actions[bot]andCrowdin Bot fd16d734fb New Crowdin translations by Github Action (#3364)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-15 12:42:58 +02:00
Hariom BalharaandOmar López 55152141ee Routing Forms - Avoid Flicker, Fix preload of Cal Font (#3368)
* Fix flicker due to Shell on Routing Form pages. Fix font preload(unrealted to routing forms)

* Update _app.tsx

Co-authored-by: Omar López <zomars@me.com>
2022-07-15 05:47:37 +00:00
75b817531e Fixes timezone in reminder templates (workflows) (#3376)
* fix time zone in reminder template

* fix timezone in email subject

* add missing timezone

* fix date in sms reminder template

* use organizer time zone if message send to host

* fix build error

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-07-14 19:05:01 -04:00
zomars 9162d90504 Restores old CI config 2022-07-14 15:47:09 -06:00
Carina WollendorferandOmar López bf2843c17e Create cron-scheduleSMSReminders.yml (#3373)
Co-authored-by: Omar López <zomars@me.com>
2022-07-14 15:22:40 -06:00
Carina Wollendorfer 6a28396499 create cron-scheduleEmailReminders (#3374) 2022-07-14 15:12:18 -06:00
zomars 1a0a318a55 Revert "Adds custom rewrite logic for hosted cal.com"
This reverts commit 9e5b1f121f.

# Conflicts:
#	apps/web/next.config.js
2022-07-14 14:56:00 -06:00
zomars 99b821edcd Update LicenseRequired.tsx 2022-07-14 14:48:50 -06:00
Peer Richelsen ded0b1410a Update LicenseRequired.tsx 2022-07-14 14:49:52 +02:00
58d1c28e9d Routing Forms (#2785)
* Add Routing logic to Query builder

* Make a working redirect

* Make it an app

* Move pages and components to App

* Integrate all pages in the app

* Integrate prisma everywhere

* Fix Routing Link

* Add routing preview

* Fixes

* Get deplouyed on preview with ts disabled

* Fix case

* add reordering for routes

* Move away from react DnD

* Add sidebar

* Add sidebar support and select support

* Various fixes and improvements

* Ignore eslint temporarly

* Route might be falsy

* Make CalNumber support required validation

* Loader improvements

* Add SSR support

* Fix few typescript issues

* More typesafety, download csv, bug fiees

* Add seo friendly link

* Avoid seding credebtials to frontend

* Self review fixes

* Improvements in app-store

* Cahnge Form layout

* Add scaffolding for app tests

* Add playwright tests and add user check in serving data

* Add CI tests

* Add route builder test

* Styling

* Apply suggestions from code review

Co-authored-by: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com>

* Changes as per loom feedback

* Increase time for tests

* Fix PR suggestions

* Import CSS only in the module

* Fix codacy issues

* Move the codebbase to ee and add PRO and license check

* Add Badge

* Avoid lodash import

* Fix TS error

* Fix lint errors

* Fix bug to merge conflicts resolution - me query shouldnt cause the Shell to go in loading state

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Agusti Fernandez Pardo <6601142+agustif@users.noreply.github.com>
2022-07-14 12:40:53 +00:00
Peer Richelsen 7ec5f01647 kbar follow up (#3361)
* added more routes to kbar

* added right direction for tooltip, moved search icon next to logo, added keyboard shortcuts to command bar

* added right direction for tooltip, moved search icon next to logo, added keyboard shortcuts to command bar

* fixed search icon for tablet

* fixed search icon for mobile

* hide keyboard shortcut legend on mobile

* extracted strings
2022-07-14 11:32:28 +00:00
github-actions[bot]andCrowdin Bot 6f073762fa New Crowdin translations by Github Action (#3362)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-14 13:12:56 +02:00
Hariom Balhara ebd4750f2d Improves UI stability of availability page. No CLS now. (#3347) 2022-07-14 10:28:46 +00:00
b1e4c23adc Fixes label to set Riverside link (#3358)
* fix label for riverside link

* add missing space

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-07-14 07:14:55 +00:00
github-actions[bot]andCrowdin Bot 03b8643d1e New Crowdin translations by Github Action (#3360)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-14 09:03:59 +02:00
318ec316d5 Adds a fully extensible command(ctrl)+k interface for Cal (#3346)
* init

* action function added

* kbar trigger placed

* UI improvements to KBar, added command + K tooltip

* renamed quick find to commandbar

* replaced window router with nextjs router

* keyboard up n down nav shows up

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-14 06:45:07 +00:00
bf7c86ffe5 Fix Caldav not valid destination calendar and only display calendars that user can edit (#3326)
* Default readOnly to false

* Display only calendars users can edit

* Remove console.log

* Remove console.log

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-13 18:16:24 -06:00
33e36e5320 Only make web3 call if credential exists (#3323)
* Add has web3 credential

* Address feedback

* Address feedback

* Fix e2e test

* Remove unused code

* Remove unused import

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-13 18:14:39 -06:00
39199e515e Workflows (#3236)
* build basic database structure and basic design

* create simple workflow list

* add editing dots to list

* add mutation to create workflows

* add createMutation on submit + redirect to editing page

* redirect to edit page when clicking on row

* add functionality to delete workflow

* add timeUnit + input validation

* add empty screen view

* add time before it triggers to description

* add multi select with checkboxes

* remove getServerSideProps

* set default time period to 24

* fetch eventypes and display in dropdown

* add functionality to update workflows + many-to-many relationship

* fix all checked event types

* add SMS reminders

* fix bug with trigger + relocate sms template

* clean code

* add model for unscheduled reminders

* fix selected eventTypes

* fixing value to show how many event types selected

* fix plural of event types in select

* add onDelete cascade for all relations

* fix errors

* add functionality to send SMS to specific number

* fix type error for timeUnit

* set default value for time unit + fix type issues

* remove console.logs

* fix error in checking if scheduled date is more than 1h in advance

* fix build errors

* add migration for workflows

* add basic UI for editing workflow steps

* add formSchema

* improve functionality to update a step

* remove console logs

* fix issue with active event types

* allow null value for time and timeUnit

* sort steps asc step number

* add action to workflow (frontend)

* add  phone number input for SMS to specific number

* use PhoneInput for number input + input validation

* improve invalid input for phone number

* improve UI of phoneInput

* Improve design and validation

* fix undefined error

* set default action when adding action

* include all team event types

* fix phone number input for editing steps

* fix update muation to add steps

* remove console logs

* fix order of steps

* functionality to delete steps

* add trigger when event is cancelled

* add custom email body

* sms and email reminder updates

* add custom emails

* add custom email subject

* send reminder email to all attendees

* update migration

* fix default value for time and timeUnit

* save email reminders to database

* clean code

* add custom template to SMS actions

* schedule emails with sendgrid

* clean code

* add workflow templates

* keep custom template saved when changing templates

* create reminder template for email

* add dot at the end of sentace for email template

* fix merge error

* fix issue that template was not saved

* include sending emails for when event is cancelled

* fix bug that email was always sent

* add templates to sms reminders

* add info that sending sms to attendees won't trigger for already exisitng bookings

* only schedule sms for attendees when smsReminderNumber exists

* only schedule sms for attendees when smsReminderNumber exists

* set scheduled of workflow reminder to false when longer than 72 hours

* add cron for email scheduling + fixes for for sms an email scheduling

* adjust step number when deleting a step

* cast to boolean with !!

* update cron job for email reminders

* update sms template

* send reminder email not to guests

* remove sendTo from workflow reminder

* fixes sending sms without name + removing sendTo everywhere

* fix undefined name in sms template

* set user name to undefined for sending sms to a specific number

* fix singular and plural for time unit

* set to edit mode when changing action and custom template is selected

* delete reminders when booking cancelled or not active anymore

* fix type errors

* fix error that deleted reminders twice

* create booking reminders for existing bookings when eventType is set active

* improve email and sms templates

* use BookingInfo type instead of calendarEvent for reminder emails

* schedule emails for already existing bookings

* add and remove reminders for new active event types and cancelled events

* connect add action button with last step

* fix step container width for mobile view

* helper functions that return options for select

* fix typo and remove comment

* clean code

* add/improve error messages for forms

* fix typo

* clean code

* improve email template

* clean code

* fix missing prop

* save reference id when scheduling reminder

* fix step not added because of changed id for new steps

* small fixes + code cleanup

* code cleanup

* show error message when number is invalid

* fix typo

* fix phone number input when location is already phone

* set multi select checkbox to read only

* change email scheduling in cron job from 7 days to 72 hours

* show active event types in workflow list

* fix trigger information for workflow list

* improve layout for small screens in workflow list

* remove optional from zod type for workflow name

* order workflows by id

* use link icon to show active event types

* fix plural and add translation for showing nr of active eventtypes

* fix text for sms reminder template

* add reminders for added steps

* remove optional for activeOn

* improve reminder templates

* improve design of custom input fields

* set edit mode to false when phone number isn't needed anymore

* set sendTo in workflow step only for SMS_NUMBER action

* set email body and subject only when custom template

* only delete reminders that belong to workflow steps

* improve text for new event book trigger

* move reminders folder to workflows

* fix issue that save button was sometimes enabled in edit mode

* fix form issues for send to

* delete all scheduled reminders when workflow is deleted

* use enum for method

* fix imports for workflow methods

* add missing import

* fix edit mode

* create reminders when event is confirmed

* add reminderScheduler to reduce duplicate code

* make workflow enterprise and pro only feature

* move all files to /ee/ folder

* move package.json change to /ee/ folder

* add pro badge to shell

* set to edit mode to true if email subject is missing when action changes

* fix loading bug

* add migration

* fix old imports

* don't schedule reminders for opt-ins

* fix style of email body

* code clean up

* Update yarn.lock

* fix isLoading for active on dropdown

* update import for prisma

Co-authored-by: Omar López <zomars@me.com>

* update imports

* remove console

* use session to check if user has valid license

* use defaultHandler

* clean up code

* Create db-staging-snapshot.yml

* move LisenceRequired inside shell

* update import for FormValues

* fix phone input design

* fix disabled save button for edit mode

* squah all migration into a single one

* use isAfter and isBefore instead of isBetween

* import dayjs from @calcom

* validate phone number for sms reminders when booking event

* Allows auto approvals for crowdin

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-07-14 00:10:45 +00:00
alannnc 44b8cedef0 Fix/main zoom schema parsing (#3275)
* zod zoom parsing only required fields

* Change logger for console.error
2022-07-13 18:01:00 -06:00
zomars c7d220e998 Preventing technical debt to break linting 2022-07-13 17:57:54 -06:00
zomars 78f0b095ae Update submodule-sync.yml 2022-07-13 16:17:28 -06:00
zomars 9a0b1832c3 Update submodule-sync.yml 2022-07-13 16:13:29 -06:00
github-actions[bot]andCrowdin Bot c8ef235916 New Crowdin translations by Github Action (#3356)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-13 23:57:20 +02:00
zomars 9760a2606e Submodule sync 2022-07-13 15:15:40 -06:00
zomars e916c67415 Linting fixes 2022-07-13 15:14:16 -06:00
zomars f6684c001c Fixes lint:fix script 2022-07-13 15:11:40 -06:00
zomars 9e5b1f121f Adds custom rewrite logic for hosted cal.com 2022-07-13 15:03:09 -06:00
zomars 179da11f4b Submodule sync 2022-07-12 16:32:31 -06:00
zomars 4bbca06bea Linting 2022-07-12 16:31:09 -06:00
zomars c5263efce2 Adds missing linting scripts 2022-07-12 16:31:09 -06:00
zomars 578c3d64ef Enables react/self-closing-comp lint rule 2022-07-12 16:31:09 -06:00
sean-brydon dbca46d677 Fixing 500 on slack OAuth (#3342) 2022-07-12 20:33:46 +00:00
zomars 1f953c5ca0 Update lint.yml 2022-07-12 14:15:06 -06:00
zomars a677ffb2a5 Update lint.yml 2022-07-12 14:11:29 -06:00
zomars 0ff6d4557a Fixing lint annotations 2022-07-12 14:05:15 -06:00
zomars 7bcc5c4548 eslint react/jsx-curly-brace-presence 2022-07-12 11:52:13 -06:00
zomars dbfc1477aa Formatting 2022-07-12 11:52:13 -06:00
Leo Giovanettiandkodiakhq[bot] b1d637d1a6 Upgrading nextjs and collect (#3252)
* Upgrading nextjs and collect

* Fixing suggestions + yarn.lock

* Updating submodules

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-12 11:43:53 -06:00
Peer Richelsen c3c7f82841 hide back button for embeds (#3339) 2022-07-12 14:33:12 +00:00
8d40e7cfd2 HotFix - Embed Flicker for newly converted static availability page (#3277)
* Add missing embed fingerprint

* Fix embed flicker for Availability page

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-07-12 14:18:53 +00:00
46266781b5 Update calendar when new attendees are added to bookings w/ seats (#3257)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-07-12 09:16:34 -04:00
zomars 5fa336c195 v1.7.7 2022-07-11 19:22:45 -06:00
alannnc 0b53cd6ffc fix/zoom schema parsing (#3274) 2022-07-11 19:22:31 -06:00
github-actions[bot]andCrowdin Bot 9ad4cba8cf New Crowdin translations by Github Action (#3324)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-11 19:15:11 -06:00
zomars 9ff4d7e024 Allows auto approvals for crowdin 2022-07-11 18:49:40 -06:00
zomars 012af52f1b Removes names for required-checks 2022-07-11 18:34:38 -06:00
zomars 6e1c165b31 Removes redundant matrix for now 2022-07-11 18:27:24 -06:00
zomars 83d2c924ca Removes matrix from required checks 2022-07-11 18:24:22 -06:00
alannnc ebc098186d Fix conditional for constant IS_SELF_HOSTED (#3329) 2022-07-11 17:46:47 -06:00
Omar López c7288bb822 Update and rename bug-hunting.yml to add-to-project.yml 2022-07-11 17:41:55 -06:00
5587867598 RFC: Custom eslint plugin (#3272)
* WIP

* Renamed pacakge

* Update yarn.lock

* Lint fixes

* Moved afterBufferTime to busyTimes & heavily simplify busy check (#3258)

* New Crowdin translations by Github Action (#3312)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>

* Fix shell flicker (#3314)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* Add recommended config to plugin

* Linting

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-07-11 17:35:50 -06:00
github-actions[bot]andCrowdin Bot 62f6373102 New Crowdin translations by Github Action (#3320)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-11 21:23:02 +02:00
github-actions[bot]andCrowdin Bot 61f29d4a86 New Crowdin translations by Github Action (#3317)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-11 17:53:01 +02:00
Hariom BalharaandPeer Richelsen 93ab0cfd09 Fix shell flicker (#3314)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-07-11 16:37:20 +02:00
github-actions[bot]andCrowdin Bot 8f495f7b67 New Crowdin translations by Github Action (#3312)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-11 16:35:34 +02:00
Alex van Andel c9d5af6591 Moved afterBufferTime to busyTimes & heavily simplify busy check (#3258) 2022-07-11 13:00:08 +02:00
github-actions[bot]andCrowdin Bot adffb00817 New Crowdin translations by Github Action (#3269)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-09 09:17:05 +02:00
Omar López afe730e502 Update README.md 2022-07-08 13:04:34 -06:00
Matt 4cd43087e9 Update google.mdx (#3268)
https://calendso.slack.com/archives/C01V37YT188/p1657289355306489
2022-07-08 08:47:15 -06:00
github-actions[bot]andCrowdin Bot 371fd48c15 New Crowdin translations by Github Action (#3265)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-08 15:25:52 +02:00
github-actions[bot]andCrowdin Bot 657cf7e180 New Crowdin translations by Github Action (#3253)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-07-08 15:21:50 +02:00
6b0ac96b38 Fix request reschedule organiser email (#3227)
* Fix request reschedule organizer email

* Updated book a new time link and removed manage or cancel action on user owner email request reschedule

* Fix let with const for a variable and change env var to a constant from lib

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-07 16:25:05 -06:00
alannnc 2c30976e99 WIP stripe customer metedada sync (#3153) 2022-07-07 15:24:42 -06:00
zomars a7212660a4 Update db-staging-snapshot.yml 2022-07-07 12:05:31 -06:00
zomars 5e4e98e002 Update db-staging-snapshot.yml 2022-07-07 11:30:48 -06:00
zomars 7bbfde607a Update db-staging-snapshot.yml 2022-07-07 11:25:47 -06:00
zomars f179c5b818 Update db-staging-snapshot.yml 2022-07-07 11:10:45 -06:00
zomars 03c27ebd62 Create db-staging-snapshot.yml 2022-07-07 11:06:35 -06:00
Carina Wollendorfer db2886d0d4 left align cancel for recurring events (#3245) 2022-07-07 17:12:13 +01:00
Hariom Balhara 5db762e987 Add logs (#3256) 2022-07-07 09:26:22 -06:00
Hariom Balhara f9cc71d120 ... doesnt seem to run dependencies first. So, ensure that using pipeline (#3254) 2022-07-07 00:00:48 -06:00
Hariom Balhara 6db129e3bf Fix Around location update (#3229) 2022-07-06 19:22:56 -06:00
sean-brydon c6b1d99270 Edit around placeholder (#3230) 2022-07-06 19:22:11 -06:00
noobyogi0010 34cd41be67 fixes #3142 - Huddle01 installation (no success message and no redirect) issue (#3231) 2022-07-06 19:21:16 -06:00
Agusti Fernandez Pardo 8ca627040b fix: adds new constants fallbacks for railway and heroku apps (#3217) 2022-07-06 19:13:02 -06:00
Bailey Pumfleet 13528ada2e Fix awaiting approval translation (#3234) 2022-07-06 19:09:28 -06:00
Alex van Andel 3c7380c3a9 Contains multiple fixes centered around Zoom (#3238) 2022-07-06 19:08:38 -06:00
zomars 9d951a5802 Formats schema 2022-07-06 16:55:29 -06:00
zomars b70550fc83 Update yarn.lock 2022-07-06 16:25:29 -06:00
zomars 6a12ff7984 Update yarn.lock 2022-07-06 16:19:54 -06:00
zomars 279d9766d8 Update website 2022-07-06 15:28:11 -06:00
85b61bed01 New Crowdin translations by Github Action (#3244)
* New Crowdin translations by Github Action

* Delete common.json

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-07-06 22:39:00 +02:00
zomars c2ca848f0a Transpiling fixes 2022-07-06 14:34:49 -06:00
c890e8d06d feature/settings-username-update (#2306)
* WIP feature/settings-username-update

* WIP username change

* WIP downgrade stripe

* stripe downgrade and prorate preview

* new UI for username premium component

* Fix server side props

* Remove migration, changed field to metadata user

* WIP for update subscriptions

* WIP intent username table

* WIP saving and updating username via hooks

* WIP saving working username sub update

* WIP, update html to work with tests

* Added stripe test for username update go to stripe

* WIP username change test

* Working test for username change

* Fix timeout for flaky test

* Review changes, remove logs

* Move input username as a self contained component

* Self review changes

* Removing unnecesary arrow function

* Removed intentUsername table and now using user metadata

* Update website

* Update turbo.json

* Update e2e.yml

* Update yarn.lock

* Fixes for self host username update

* Revert yarn lock from main branch

* E2E fixes

* Centralizes username check

* Improvements

* WIP separate logic between premium and save username button

* WIP refactor username premium update

* Saving WIP

* WIP redo of username check

* WIP obtain action normal, update or downgrade

* Update username change components

* Fix test for change-username self host or cal server

* Fix user type for premiumTextfield

* Using now a global unique const to know if is selfhosted, css fixes

* Remove unused import

* Using dynamic import for username textfield, prevent submit on enter

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-06 13:31:07 -06:00
Carina WollendorferandCarinaWolli 7d6a6bf812 add error message for invalid phone number (#3249)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-07-06 13:01:16 -06:00
Omar López a4684042a6 Matches phone input darkmode to the rest of inputs (#3247) 2022-07-06 12:19:48 -06:00
zomars 56da9c7c5f Removed forward slashes 2022-07-06 11:33:36 -06:00
zomars 7967c6e46b updates GH workflow 2022-07-06 11:31:34 -06:00
zomars 8d96b79a5b Update GH workflows 2022-07-06 09:28:11 -06:00
sean-brydon d0325c13d1 Split on slug to get profile page (#3228)
* Split on slug to get profile page

* Fix teams slug

* Remove console.log

* Remove unneeded diff
2022-07-06 08:47:22 -06:00
Hariom Balhara 5ea08c237a Fix ordering of credentials (#3243) 2022-07-06 08:43:57 -06:00
Hariom Balhara b3f7e8e322 Booking Module Improvements (#3221)
* Fix momentary render of dates before skeleton

* Fix Hydration issue - We shouldnt guess timezone on server as it would be different from client causing hydration error

* Fix wrong trpcQuery result when clicking on a particular date. Also, fix last day of the month being never available

* Fix as per PR Feedback
2022-07-06 08:25:41 -06:00
zomars b7d4e3885e Update bug-hunting.yml 2022-07-05 10:06:41 -06:00
zomars 3c324eb776 Update labeler.yml 2022-07-04 20:10:56 -06:00
zomars c16e71aa01 Update required-checks.yml 2022-07-04 19:59:38 -06:00
zomars 5fa3f45fed Update required-checks.yml 2022-07-04 19:49:51 -06:00
zomars 450d0e7f05 Adds timeout to checks 2022-07-04 19:48:15 -06:00
zomars b5745fd0e5 Update required-checks.yml 2022-07-04 19:44:23 -06:00
Omar López bac12d0cbd Update CONTRIBUTING.md (#3226)
* Update CONTRIBUTING.md

* Don't run e2e for md files

* Debugging why is not ignoring e2e test
2022-07-04 19:41:22 -06:00
zomars f17833552e Test 2022-07-04 19:29:07 -06:00
zomars 15bdcaff88 Debugging why is not ignoring e2e test 2022-07-04 19:22:38 -06:00
zomars 29507557d6 Update required-checks.yml 2022-07-04 19:19:01 -06:00
zomars 30c1dd036d Update lint.yml 2022-07-04 19:13:51 -06:00
zomars 8f24b92666 Update required-checks.yml 2022-07-04 18:57:06 -06:00
zomars 7c99004225 Update required-checks.yml 2022-07-04 18:50:49 -06:00
zomars 8bce6055c7 Update required-checks.yml 2022-07-04 18:48:08 -06:00
zomars 049e433781 Path fixes 2022-07-04 18:34:39 -06:00
zomars 3ac8caecbf Don't run e2e for md files 2022-07-04 18:30:44 -06:00
zomars e394cfdcd8 Update checks 2022-07-04 18:26:27 -06:00
zomars be99035f49 GH workflow name collision fixes 2022-07-04 17:36:38 -06:00
zomars 6c8e624496 Update required-checks.yml 2022-07-04 15:31:28 -06:00
zomars 5369d4123e Prevent name conflicts for e2e 2022-07-04 15:27:22 -06:00
zomars 3a055018f6 Testing required checks 2022-07-04 14:29:20 -06:00
zomars 6ac670b366 Fixes bug hunting file location 2022-07-04 14:19:05 -06:00
zomars db35a7c68f Update bug-hunting.yml 2022-07-04 13:58:32 -06:00
zomars a1d8fb0b46 Auto adds bugs to bug hunting view 2022-07-04 13:57:05 -06:00
zomars 7170fadff8 Adds pull request labeler 2022-07-04 13:16:01 -06:00
zomars fa5e6b42de v1.7.6 2022-07-04 10:51:26 -06:00
zomars 1f02a5c718 Update yarn.lock 2022-07-04 10:48:21 -06:00
zomars c55b17bf58 Update console 2022-07-04 10:48:16 -06:00
zomars 4975493090 Matches zod version to the rest 2022-07-04 10:47:12 -06:00
zomars 785a014f3c Submodule sync 2022-07-04 10:29:33 -06:00
depfu[bot] dd941e62ef Update next-auth to version 4.5.0 (#3138) 2022-07-04 14:36:20 +00:00
sean-brydonandPeer Richelsen eec2d8ceed Implement profile back button (#3165)
* Implement profile backbutton

* update slug to use username

* Back Button Improvments

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-07-04 08:26:22 -06:00
Alex van Andel b546d9e33e Some tweaks to slots to fix server time effecting logic (#3218) 2022-07-02 10:13:39 -06:00
Joe Au-Yeung 201d17264c Attach credential id to destinationCalendar records (#3197) 2022-07-01 21:55:27 +01:00
2cbf46a323 Seats events send email to attendees and organizer (#3206)
* Add attendees to email notfications

* New seat title

* Change organizer email

* Clean console.log

* Fix type errors

* Update packages/emails/src/templates/OrganizerScheduledEmail.tsx

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-07-01 19:19:23 +00:00
Leo Giovanetti 833791bc9a Team event type scroll, manage team invite from mobile (#3171) 2022-07-01 12:29:59 -06:00
00c9df7602 There is lot of hardcoding links in application (#3130)
* There is lot of hardcoding links in application

* Apply suggestions from code review

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

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2022-07-01 11:19:52 -06:00
Omar LópezandPeer Richelsen 669199e273 Removes unused files (#3208)
* Adds next-unused

* Cleanup

* Update next-unused

* Removes next-unused

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-07-01 10:02:44 -06:00
93e2246b18 Wrong color for Timezone selector on booking page (#3187)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-07-01 12:28:17 +01:00
James VansteenkisteandBailey Pumfleet f639405c20 fix: event creation on external office calendar (#3152)
* fix: event creation on external office calendar

* chore: simplify fix implementation

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-07-01 12:26:20 +01:00
zomars 240fc67d72 v1.7.5 2022-06-30 10:32:55 -06:00
Hariom Balhara eef405ef41 If booking doesnt exist its 404 page but if it exists and eventType doesnt exist, log it for now and continue with the flow (#3202) 2022-06-30 09:09:15 -06:00
Hariom BalharaandLeo Giovanetti 1469b856a6 Fix hubspot callback (#3195)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2022-06-30 07:01:07 +00:00
Alex van Andel 53bbbf793d Fixes the slow performance due to the Dayjs timezone plugin (#3201)
* Fixes the slow performance due to the Dayjs timezone plugin

* Bye.

* Parses busyTime in UTC
2022-06-29 18:33:19 -06:00
Alex van Andel a91f251b98 Remove '{}' to prevent overwriting .env.appStore value (#3200) 2022-06-29 16:56:31 -06:00
github-actions[bot]andCrowdin Bot d95c3a1459 New Crowdin translations by Github Action (#3196)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-06-29 22:57:58 +02:00
Omar López 92bfb3e5b9 Updates snaplet transform script (#3199)
* Update .gitignore

* Fixes local dev env checks

* Update transform.ts
2022-06-29 13:03:13 -06:00
Hariom Balharaandkodiakhq[bot] fbc4eb5457 Set theme initially so that the gneerated JS code knows the correct theme already (#3193)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-29 12:50:46 -06:00
Carina WollendorferandCarinaWolli 4a74abcb34 change meeting url to a link (#3198)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-06-29 12:22:07 -06:00
Hariom Balhara 09c8f55682 Make i18n a non-batch request and non-cacheable (#3188) 2022-06-29 10:50:32 -06:00
alannnc 3f3ffa2ba8 fix/availability-tz-offset (#3185) 2022-06-29 09:01:30 +00:00
Peer Richelsen 654cfbb653 updated readme, new gif, added roadmap banner 2022-06-29 10:31:08 +02:00
Hariom Balhara ad35d3d567 Make the height of textarea as per container height but consider the text above it (#3189) 2022-06-29 10:06:36 +02:00
zomars 70a31874f0 v1.7.4 2022-06-28 17:12:56 -06:00
zomars fe8b0fc5f8 Possible timeout fix take 3 2022-06-28 16:53:14 -06:00
zomars 2983fb4c12 Fixes month name 2022-06-28 16:24:53 -06:00
zomars 03cc4855a8 Update package.json 2022-06-28 16:01:50 -06:00
zomars c82a9853d2 Submodule sync 2022-06-28 15:58:14 -06:00
zomars d732a984c2 Merge branch 'main' of https://github.com/calcom/cal.com 2022-06-28 15:46:17 -06:00
zomars 826583f404 Second potential fix for timeout 2022-06-28 15:46:07 -06:00
zomars b6dd0d5804 Remove SSG prefetch 2022-06-28 15:32:40 -06:00
Leo Giovanetti df2c3c42bf Recurring config missing from event (#3182) 2022-06-28 18:04:44 -03:00
zomars 7122a4a91d Consolidates dayjs in a single package 2022-06-28 14:48:24 -06:00
zomars 5be669a479 Update zod-utils.ts 2022-06-28 13:33:55 -06:00
zomars 8629d92a64 Merge branch 'main' of https://github.com/calcom/cal.com 2022-06-28 13:18:53 -06:00
zomars 840b6c498a Potential fix for availability timeout 2022-06-28 13:18:47 -06:00
zomars 7a8b9e5f32 Potential fix for availability timeout 2022-06-28 13:17:43 -06:00
zomars fe50f04075 Only stale SSR requests 2022-06-28 11:37:01 -06:00
Leo Giovanetti e1e369de9e Hotfix: Fixing app types variants (fixes Hubspot) (#3176)
* Fixing Hubspot

* Comment
2022-06-28 16:36:19 +00:00
9b2ad3f350 Negative days in trial license period (#3133)
* Negative days in trial license period

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

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-06-28 10:03:31 -06:00
GlitchWitch 83dfafb8a1 Create SECURITY.md (#3174)
* Create SECURITY.md

Creates a SECURITY.md file based on https://cal.com/security

* Update markdown formatting
2022-06-28 17:24:11 +02:00
2cd7ddcdbe New Crowdin translations by Github Action (#3173)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-06-28 15:27:29 +02:00
Esteban Dalel R 2fa9ea4501 Fix broken link (#3172) 2022-06-28 11:48:16 +01:00
Peer Richelsen 6fff27b5ad Update README.md 2022-06-28 09:54:37 +02:00
zomars 0794224288 Prevents undefined classNames in Skeletons 2022-06-27 21:04:10 -06:00
alannnc e3e1de3c11 Fix saving price and currency (#3168) 2022-06-27 20:31:25 -06:00
zomars 52759333fd Skeleton fixes 2022-06-27 20:25:06 -06:00
zomars e27a2bafcd AvailabilityPage cleanup 2022-06-27 20:17:23 -06:00
zomars 87befdbdf4 Adds perf metrics for each tRPC endpoint 2022-06-27 19:19:33 -06:00
413 changed files with 12894 additions and 10210 deletions
+2 -1
View File
@@ -21,7 +21,8 @@ DAILY_SCALE_PLAN=''
# - GOOGLE CALENDAR/MEET/LOGIN
# Needed to enable Google Calendar integration and Login with Google
# @see https://github.com/calendso/calendso#obtaining-the-google-api-credentials
GOOGLE_API_CREDENTIALS='{}'
GOOGLE_API_CREDENTIALS=
# To enable Login with Google you need to:
# 1. Set `GOOGLE_API_CREDENTIALS` above
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
+14 -1
View File
@@ -77,16 +77,29 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=
# Sengrid
# Used for email reminders in workflows
SENDGRID_API_KEY=
SENDGRID_EMAIL=
# Twilio
# Used to send SMS reminders in workflows
TWILIO_SID=
TWILIO_TOKEN=
TWILIO_MESSAGING_SID=
# This is used so we can bypass emails in auth flows for E2E testing
# Set it to "1" if you need to run E2E tests locally
NEXT_PUBLIC_IS_E2E=
# Used for internal billing system
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRODUCT=
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE=
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRO_PLAN_PRODUCT_ID=
STRIPE_PREMIUM_PLAN_PRODUCT_ID=
STRIPE_FREE_PLAN_PRODUCT_ID=
# Use for internal Public API Keys and optional
API_KEY_PREFIX=cal_
+5
View File
@@ -0,0 +1,5 @@
"♻️ autoupdate":
- apps/web/public/static/locales/**/*.json
automerge:
- apps/web/public/static/locales/**/*.json
+22
View File
@@ -0,0 +1,22 @@
name: Add bugs to Bug Hunting
on:
issues:
types:
- opened
- labeled
pull_request:
types:
- opened
- labeled
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.1.0
with:
project-url: https://github.com/orgs/calcom/projects/1
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
labeled: 🐛 bug, 🎨 needs design, 🙋🏻‍♂️help wanted, 🚨 urgent
+6 -10
View File
@@ -3,15 +3,11 @@ on:
pull_request_target:
branches:
- main
paths:
- "**.tsx?"
jobs:
types:
name: Check types
strategy:
matrix:
node: ["16.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
check-types:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v2
@@ -19,10 +15,10 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: Use Node ${{ matrix.node }}
- name: Use Node 16.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
node-version: 16.x
cache: "yarn"
- run: yarn --frozen-lockfile
- run: yarn type-check
+1 -1
View File
@@ -7,7 +7,7 @@ on:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
jobs:
cron:
cron-bookingReminder:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
+1 -1
View File
@@ -7,7 +7,7 @@ on:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
jobs:
cron:
cron-downgradeUsers:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
@@ -0,0 +1,23 @@
name: Cron - scheduleEmailReminders
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
jobs:
cron-scheduleEmailReminders:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/scheduleEmailReminders \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
@@ -0,0 +1,23 @@
name: Cron - scheduleSMSReminders
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
jobs:
cron-scheduleSMSReminders:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/scheduleSMSReminders \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail
+18
View File
@@ -0,0 +1,18 @@
name: Staging DB Snapshot
on:
schedule:
- cron: "0 4 * * 1-5" # At 04:00 on every day-of-week from Monday through Friday.
workflow_dispatch:
jobs:
db-staging-snapshot:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Snaplet CLI
run: curl -sL https://app.snaplet.dev/get-cli/ | bash
- name: Restore Snapshot
run: snaplet snapshot restore -y
env:
SNAPLET_DATABASE_URL: ${{ secrets.SNAPLET_DATA_TARGET_DB_URL }}
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
SNAPLET_DATABASE_ID: ${{ secrets.SNAPLET_DATABASE_ID }}
+99
View File
@@ -0,0 +1,99 @@
name: E2E App-Store Apps
on:
push:
branches: [ feature/event-routing ]
pull_request_target: # So we can test on forks
branches:
- main
paths-ignore:
- apps/api/**
- apps/console/**
- apps/docs/**
- apps/swagger/**
- apps/website/**
- apps/web/public/**
jobs:
test:
timeout-minutes: 20
name: E2E App-Store Apps
strategy:
matrix:
node: ["16.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
NEXTAUTH_SECRET: secret
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: true
# CRON_API_KEY: xxx
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
PAYMENT_FEE_PERCENTAGE: 0.005
PAYMENT_FEE_FIXED: 10
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
SAML_ADMINS: pro@example.com
NEXTAUTH_URL: http://localhost:3000/api/auth
NEXT_PUBLIC_IS_E2E: 1
# EMAIL_FROM: e2e@cal.com
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
# MS_GRAPH_CLIENT_ID: xxx
# MS_GRAPH_CLIENT_SECRET: xxx
# ZOOM_CLIENT_ID: xxx
# ZOOM_CLIENT_SECRET: xxx
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
services:
postgres:
image: postgres:12.1
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
ports:
- 5432:5432
steps:
- name: Checkout repo
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
fetch-depth: 2
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: "yarn"
- name: Cache playwright binaries
uses: actions/cache@v2
id: playwright-cache
with:
path: |
~/Library/Caches/ms-playwright
~/.cache/ms-playwright
${{ github.workspace }}/node_modules/playwright
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
restore-keys: cache-playwright-
- run: yarn --frozen-lockfile
- name: Install playwright deps
# if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps
- name: Run Tests
run: yarn app-e2e-quick
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results-core
path: packages/app-store/**/playwright/results
+11 -19
View File
@@ -1,27 +1,16 @@
name: E2E tests Embed
name: E2E Embed tests and booking flow(for non-embed as well)
on:
pull_request_target: # So we can test on forks
branches:
- main
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
paths-ignore:
- apps/api/**
- apps/console/**
- apps/docs/**
- apps/swagger/**
- apps/website/**
- apps/web/public/**
- tests/**
- playwright/**
paths:
- apps/web/**
- packages/embeds/**
jobs:
test:
e2e-embed:
timeout-minutes: 20
name: Embed and booking flow(for non-embed as well)
strategy:
matrix:
node: ["16.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
@@ -33,6 +22,9 @@ jobs:
# CRON_API_KEY: xxx
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
@@ -69,10 +61,10 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
fetch-depth: 2
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- name: Use Node ${{ matrix.node }}
- name: Use Node 16.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
node-version: 16.x
cache: "yarn"
- name: Cache playwright binaries
+13 -11
View File
@@ -3,17 +3,13 @@ on:
pull_request_target: # So we can test on forks
branches:
- main
paths-ignore:
- apps/web/public/static/locales/**
paths:
- apps/web/**
- packages/**
jobs:
test:
e2e:
timeout-minutes: 20
name: Testing ${{ matrix.node }} and ${{ matrix.os }}
strategy:
matrix:
node: ["16.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
@@ -25,9 +21,15 @@ jobs:
# CRON_API_KEY: xxx
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
STRIPE_PRO_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PRO_PLAN_PRODUCT_ID }}
STRIPE_PREMIUM_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_PREMIUM_PLAN_PRODUCT_ID }}
STRIPE_FREE_PLAN_PRODUCT_ID: ${{ secrets.CI_STRIPE_FREE_PLAN_PRODUCT_ID }}
PAYMENT_FEE_PERCENTAGE: 0.005
PAYMENT_FEE_FIXED: 10
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
@@ -61,10 +63,10 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
fetch-depth: 2
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- name: Use Node ${{ matrix.node }}
- name: Use Node 16.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
node-version: 16.x
cache: "yarn"
- name: Cache playwright binaries
+14
View File
@@ -0,0 +1,14 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
+9 -9
View File
@@ -3,13 +3,12 @@ on:
pull_request_target:
branches:
- main
paths:
- "**.tsx?"
- "**.jsx?"
jobs:
lint:
strategy:
matrix:
node: ["16.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
steps:
- name: Checkout repo
@@ -18,10 +17,10 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: Use Node ${{ matrix.node }}
- name: Use Node 16.x
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
node-version: 16.x
cache: "yarn"
- name: Install deps
@@ -36,10 +35,11 @@ jobs:
run: jq -s '[.[]]|flatten' lint-results/*.json &> lint-results/eslint_report.json
- name: Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@1.2.0
uses: ataylorme/eslint-annotate-action@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
repo-token: ${{ secrets.GITHUB_TOKEN }}
report-json: "lint-results/eslint_report.json"
only-pr-files: false
- name: Upload ESLint report
if: ${{ always() }}
+36
View File
@@ -0,0 +1,36 @@
name: "Meta Workflow: Require Conditional Status Checks"
on:
pull_request:
branches:
- main
jobs:
required-checks:
runs-on: ubuntu-latest
steps:
- name: Ensure All Conditional Checks Have Passed
uses: blend/require-conditional-status-checks@2022.02.04
with:
timeout: 15m
github-token: ${{ github.token }}
interval: 20s
# Question marks aren't working
# @see https://github.com/blend/require-conditional-status-checks/pull/11
checks-yaml: |
- job: lint
paths:
- /**/*.jsx
- /**/*.js
- /**/*.tsx
- /**/*.ts
- job: check-types
paths:
- /**/*.tsx
- /**/*.ts
- job: e2e
paths:
- /apps/web/**
- /packages/**
- job: e2e-embed
paths:
- /apps/web/**
- /packages/embeds/**
+7 -8
View File
@@ -3,7 +3,6 @@ on:
schedule:
- cron: "15 */4 * * *"
workflow_dispatch: ~
jobs:
submodule-sync:
name: Submodule update
@@ -11,11 +10,11 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: run action
uses: releasehub-com/github-action-create-pr-parent-submodule@v1
with:
github_token: ${{ secrets.GH_ACCESS_TOKEN }}
parent_repository: "calcom/cal.com"
checkout_branch: "main"
pr_against_branch: "main"
owner: "calcom"
submodules: recursive
token: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Commit
run: |
git config user.email "actions@github.com"
git config user.name "github-actions"
git commit -am "Auto updated submodule references" && git push || echo "No changes to commit"
+1 -2
View File
@@ -4,5 +4,4 @@ version = 1
autoupdate_label = "♻️ autoupdate"
[approve]
auto_approve_usernames = ["dependabot"]
auto_approve_usernames = ["dependabot", "github-actions"]
+32 -32
View File
@@ -1,7 +1,7 @@
// This transform config was generated by Snaplet.
// Snaplet found fields that may contain personally identifiable information (PII)
// and used that to populate this file.
import { copycat } from "@snaplet/copycat";
import { copycat as c } from "@snaplet/copycat";
import type { Transform } from "./structure";
@@ -11,13 +11,13 @@ function hasStringProp<T extends string>(x: unknown, key: T): x is { [key in T]:
function replaceKeyIfExists<T extends string>(x: object, key: T) {
if (hasStringProp(x, key)) {
return { ...x, [key]: copycat.uuid(x[key]) };
return { ...x, [key]: c.uuid(x[key]) };
}
return x;
}
function generateSlug(x: string) {
return copycat.words(x, { max: 3 }).split(" ").join("-");
return c.words(x, { max: 3 }).split(" ").join("-");
}
function replaceSensitiveKeys(record: object) {
@@ -39,57 +39,57 @@ function replaceSensitiveKeys(record: object) {
};
}
const generateUsername = (x: string) => `${c.firstName(x)}-${c.lastName(x)}${c.int(x, { min: 2, max: 99 })}`;
const config: Transform = () => ({
public: {
_user_eventtype: ({ row }) => ({
A: copycat.int(row.A),
B: copycat.int(row.B),
}),
ApiKey: ({ row }) => ({
note: copycat.sentence(row.note),
hashedKey: c.uuid(row.hashedKey),
}),
App: ({ row }) => ({
keys: replaceSensitiveKeys(row.keys),
}),
Attendee: ({ row }) => ({
email: copycat.email(row.email),
name: copycat.fullName(row.name),
timeZone: copycat.timezone(row.timeZone),
}),
Availability: ({ row }) => ({
// date: copycat.fullName(row.date),
email: c.email(row.email),
name: c.fullName(row.name),
timeZone: c.timezone(row.timeZone),
}),
Credential: ({ row }) => ({
key: typeof row.key === "string" ? copycat.uuid(row.key) : replaceSensitiveKeys(row.key),
key: typeof row.key === "string" ? c.uuid(row.key) : replaceSensitiveKeys(row.key),
}),
EventType: ({ row }) => ({
slug: generateSlug(row.slug),
timeZone: copycat.timezone(row.timeZone),
}),
Feedback: ({ row }) => ({
// date: copycat.dateString(row.date),
timeZone: c.timezone(row.timeZone),
eventName: c.words(row.eventName, { max: 3 }),
}),
ResetPasswordRequest: ({ row }) => ({
email: copycat.email(row.email),
email: c.email(row.email),
}),
Schedule: ({ row }) => ({
timeZone: copycat.timezone(row.timeZone),
timeZone: c.timezone(row.timeZone),
}),
Team: ({ row }) => ({
bio: copycat.sentence(row.bio),
name: copycat.words(row.name, { max: 2 }),
bio: c.sentence(row.bio),
name: c.words(row.name, { max: 2 }),
slug: generateSlug(row.slug),
}),
users: ({ row }) => ({
bio: copycat.sentence(row.bio),
email: copycat.email(row.email),
name: copycat.fullName(row.name),
password: copycat.password(row.password),
timeZone: copycat.timezone(row.timeZone),
username: generateSlug(row.username),
}),
users: ({ row }) =>
row.role !== "ADMIN"
? {
bio: c.sentence(row.bio),
email: c.email(row.email),
name: c.fullName(row.name),
password: c.password(row.password),
timeZone: c.timezone(row.timeZone),
username: generateUsername(row.username),
}
: row,
VerificationToken: ({ row }) => ({
token: copycat.uuid(row.token),
token: c.uuid(row.token),
}),
Account: ({ row }) => ({
access_token: c.uuid(row.access_token),
refresh_token: c.uuid(row.refresh_token),
}),
},
});
-4
View File
@@ -22,10 +22,6 @@
"group": {
"kind": "build",
"isDefault": true
},
// Try start the task on folder open
"runOptions": {
"runOn": "folderOpen"
}
},
{
+1 -1
View File
@@ -1,6 +1,6 @@
# Contributing to Cal.com
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
+34 -3
View File
@@ -44,7 +44,7 @@
## About The Project
<img width="100%" alt="booking-screen" src="https://user-images.githubusercontent.com/8019099/134363898-4b29e18f-3e61-42b7-95bc-10891056249d.gif">
<img width="100%" alt="booking-screen" src="https://user-images.githubusercontent.com/8019099/176390354-f1bc7069-0341-437a-9bb8-eb41092b4016.gif">
# Scheduling infrastructure for absolutely everyone
@@ -94,7 +94,7 @@ Here is what you need to be able to run Cal.
### Setup
1. Clone the repo into a public GitHub repository (to comply with AGPLv3. To clone in a private repository, [acquire a commercial license](https://cal.com/sales))
1. Clone the repo into a public GitHub repository (or fork https://github.com/calcom/cal.com/fork). If you plan to distribute the code, keep the source code public to comply with [AGPLv3](https://github.com/calcom/cal.com/blob/main/LICENSE). To clone in a private repository, [acquire a commercial license](https://cal.com/sales))
```sh
git clone https://github.com/calcom/cal.com.git
@@ -278,11 +278,17 @@ The Docker configuration can be found [in our docker repository](https://github.
You can deploy Cal on [Railway](https://railway.app/) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal on their platform.
### Vercel
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_SECRET,NEXTAUTH_URL,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT,CALCOM_LICENSE_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com)
<!-- ROADMAP -->
## Roadmap
See the [roadmap project](https://github.com/orgs/calcom/projects/1) for a list of proposed features (and known issues). You can change the view to see planned tagged releases.
<a href="https://cal.com/roadmap"><img src="https://user-images.githubusercontent.com/8019099/176388295-25081ca4-207e-4468-8079-37b195fa8e59.png" alt="Cal.com Roadmap" /></a>
See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases.
<!-- CONTRIBUTING -->
@@ -418,6 +424,31 @@ Next make sure you have your app running `yarn dx`. Then in the slack chat type
1. Select all events for the webhook you interested, e.g. `sleep_created`
1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file.
## Workflows
### Setting up SendGrid for Email reminders
1. Create a SendGrid account (https://signup.sendgrid.com/)
2. Go to Settings -> API keys and create an API key
3. Copy API key to your .env file into the SENDGRID_API_KEY field
4. Go to Settings -> Sender Authentication and verify a single sender
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
### Setting up Twilio for SMS reminders
1. Create a Twilio account (https://www.twilio.com/try-twilio)
2. Click ‘Get a Twilio phone number’
3. Copy Account SID to your .env file into the TWILIO_SID field
4. Copy Auth Token to your .env file into the TWILIO_TOKEN field
5. Create a messaging service (Develop -> Messaging -> Services)
6. Choose any name for the messaging service
7. Click 'Add Senders'
8. Choose phone number as sender type
9. Add the listed phone number
9. Leave all other fields as they are
10. Complete setup and click ‘View my new Messaging Service’
11. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
<!-- LICENSE -->
## License
+39
View File
@@ -0,0 +1,39 @@
# Security
Contact: security@cal.com
Based on [https://supabase.com/.well-known/security.txt](https://supabase.com/.well-known/security.txt)
At Cal.com, we consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our clients and our systems.
## Out of scope vulnerabilities:
- Clickjacking on pages with no sensitive actions.
- Unauthenticated/logout/login CSRF.
- Attacks requiring MITM or physical access to a user's device.
- Any activity that could lead to the disruption of our service (DoS).
- Content spoofing and text injection issues without showing an attack vector/without being able to modify HTML/CSS.
- Email spoofing
- Missing DNSSEC, CAA, CSP headers
- Lack of Secure or HTTP only flag on non-sensitive cookies
- Deadlinks
## Please do the following:
- E-mail your findings to [security@cal.com](mailto:security@cal.com).
- Do not run automated scanners on our infrastructure or dashboard. If you wish to do this, contact us and we will set up a sandbox for you.
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data,
- Do not reveal the problem to others until it has been resolved,
- Do not use attacks on physical security, social engineering, distributed denial of service, spam or applications of third parties,
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation.
## What we promise:
- We will respond to your report within 3 business days with our evaluation of the report and an expected resolution date,
- If you have followed the instructions above, we will not take any legal action against you in regard to the report,
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission,
- We will keep you informed of the progress towards resolving the problem,
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise), and
- We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.
+4 -3
View File
@@ -4,19 +4,20 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "next build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "PORT=4000 next",
"lint": "next lint",
"type-check": "tsc --pretty --noEmit",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
"start": "PORT=4000 next start",
"build": "next build"
"type-check": "tsc --pretty --noEmit"
},
"author": "Cal.com, Inc.",
"license": "MIT",
"dependencies": {
"iframe-resizer-react": "^1.1.0",
"next": "^12.1.6",
"next": "^12.2.0",
"nextra": "^1.1.0",
"nextra-theme-docs": "^1.2.2",
"react": "^18.1.0",
+1 -1
View File
@@ -25,7 +25,7 @@ To remove a product from your account that isn't listed in your Google Account,
6. In the third page (Test Users), add the Google account(s) you'll using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured.
7. Now select [Credentials](https://console.cloud.google.com/apis/credentials) from the side pane and then select Create Credentials. Select the OAuth Client ID option.
8. Select Web Application as the Application Type.
9. Under Authorized redirect URI's, select Add URI and then add the URI `<CALENDSO URL>/api/integrations/googlecalendar/callback` replacing CALENDSO URL with the URI at which your application runs.
9. Under Authorized redirect URI's, select Add URI and then add the two URIs `<CALENDSO URL>/api/integrations/googlecalendar/callback` and `<CALENDSO URL>/api/auth/callback/google` replacing CALENDSO URL with the URI at which your application runs.
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
+1 -1
View File
@@ -18,7 +18,7 @@ You need a PostgresDB database hosted somewhere. [Heroku](https://www.heroku.com
git clone https://github.com/<your-fork>/cal.com.git
```
2. Copy the `.env.example` file in `apps/web`, rename it to `.env` and fill it with your settings ([See manual setup](https://github.com/calcom/cal.com#manual) and [Obtaining the Google API Credentials](https://github.com/calcom/cal.com#obtaining-the-google-api-credentials))
2. Copy the `.env.example` file in `apps/web`, rename it to `.env` and fill it with your settings ([See manual setup](https://github.com/calcom/cal.com#manual-setup) and [Obtaining the Google API Credentials](https://github.com/calcom/cal.com#obtaining-the-google-api-credentials))
3. Install packages with `yarn`
+5 -3
View File
@@ -1,3 +1,5 @@
import { SEO_IMG_DEFAULT, DOCS_URL } from "@calcom/lib/constants";
const themeConfig = {
github: "https://github.com/calcom/cal.com",
docsRepositoryBase: "https://github.com/calcom/cal.com/blob/main/apps/docs/pages",
@@ -18,11 +20,11 @@ const themeConfig = {
content="Explore advice and explanations for all of our features, and discover new tips and tricks to get the most out of your subscription."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://cal.com/og-image.png" />
<meta name="twitter:image" content={SEO_IMG_DEFAULT} />
<meta name="twitter:site:domain" content="docs.cal.com" />
<meta name="twitter:url" content="https://docs.cal.com" />
<meta name="twitter:url" content={DOCS_URL} />
<meta name="og:title" content="Cal.com Documentation" />
<meta name="og:image" content="https://cal.com/og-image.png" />
<meta name="og:image" content={SEO_IMG_DEFAULT} />
<meta name="apple-mobile-web-app-title" content="Cal.com Docs" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
+6 -2
View File
@@ -3,14 +3,18 @@
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "PORT=4200 next dev",
"build": "next build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "PORT=4200 next dev",
"lint": "next lint",
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
"lint:report": "eslint . --format json --output-file ../../lint-results/swagger.json",
"start": "PORT=4200 next start"
},
"dependencies": {
"highlight.js": "^11.5.1",
"isarray": "2.0.5",
"next": "12.1.6",
"next": "12.2.0",
"openapi-snippet": "^0.13.0",
"react": "18.1.0",
"react-dom": "18.1.0",
+8 -15
View File
@@ -1,24 +1,17 @@
{
"extends": "@calcom/tsconfig/nextjs.json",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"paths": {
"@lib/*": ["lib/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": [
"next-env.d.ts",
"../../packages/types/*.d.ts",
"../../packages/types/next-auth.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": ["node_modules"]
}
@@ -50,7 +50,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
}));
return (
<Select
name={"additionalCalendar"}
name="additionalCalendar"
placeholder={t("connect_additional_calendar")}
options={options}
styles={{
+239 -183
View File
@@ -12,15 +12,20 @@ import { ChevronLeftIcon } from "@heroicons/react/solid";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { App as AppType } from "@calcom/types/App";
import { Button, SkeletonButton } from "@calcom/ui";
import LicenseRequired from "@ee/components/LicenseRequired";
import { trpc } from "@lib/trpc";
import Shell from "@components/Shell";
import Badge from "@components/ui/Badge";
export default function App({
const Component = ({
name,
type,
logo,
@@ -36,25 +41,19 @@ export default function App({
email,
tos,
privacy,
}: {
name: string;
type: AppType["type"];
isGlobal?: AppType["isGlobal"];
logo: string;
body: React.ReactNode;
categories: string[];
author: string;
pro?: boolean;
price?: number;
commission?: number;
feeType?: AppType["feeType"];
docs?: string;
website?: string;
email: string; // required
tos?: string;
privacy?: string;
}) {
isProOnly,
}: Parameters<typeof App>[0]) => {
const { t } = useLocale();
const { data: user } = trpc.useQuery(["viewer.me"]);
const mutation = useAddAppMutation(null, {
onSuccess: () => {
showToast("App successfully installed", "success");
},
onError: (error) => {
if (error instanceof Error) showToast(error.message || "App could not be installed", "error");
},
});
const priceInDollar = Intl.NumberFormat("en-US", {
style: "currency",
@@ -90,176 +89,233 @@ export default function App({
getInstalledApp(type);
}, [type]);
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
return (
<>
<Shell large isPublic>
<div className="-mx-4 md:-mx-8">
<div className="bg-gray-50 px-8">
<Link href="/apps">
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
<ChevronLeftIcon className="h-5 w-5" /> {t("browse_apps")}
</a>
</Link>
<div className="items-center justify-between py-4 sm:flex sm:py-8">
<div className="flex">
<img className="h-16 w-16 rounded-sm" src={logo} alt={name} />
<header className="px-4 py-2">
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
<h2 className="text-sm text-gray-500">
<span className="capitalize">{categories[0]}</span> • {t("published_by", { author })}
</h2>
</header>
</div>
<div className="mt-4 sm:mt-0 sm:text-right">
{!isLoading ? (
isGlobal || (installedAppCount > 0 && allowedMultipleInstalls) ? (
<div className="space-x-3">
<Button StartIcon={CheckIcon} color="secondary" disabled>
{installedAppCount > 0
? t("active_install", { count: installedAppCount })
: t("globally_install")}
</Button>
<InstallAppButton
type={type}
render={(buttonProps) => (
<Button StartIcon={PlusIcon} data-testid="install-app-button" {...buttonProps}>
{t("add_another")}
</Button>
)}
/>
</div>
) : (
<InstallAppButton
type={type}
render={(buttonProps) => (
<Button data-testid="install-app-button" {...buttonProps}>
{t("install_app")}
</Button>
)}
/>
)
) : (
<SkeletonButton width="24" height="10" />
)}
{price !== 0 && (
<small className="block text-right">
{feeType === "usage-based"
? commission + "% + " + priceInDollar + "/booking"
: priceInDollar}
{feeType === "monthly" && "/" + t("month")}
</small>
)}
return (
<div className="-mx-4 md:-mx-8">
<div className="bg-gray-50 px-8">
<Link href="/apps">
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
<ChevronLeftIcon className="h-5 w-5" /> {t("browse_apps")}
</a>
</Link>
<div className="items-center justify-between py-4 sm:flex sm:py-8">
<div className="flex">
<img className="h-16 w-16 rounded-sm" src={logo} alt={name} />
<header className="px-4 py-2">
<div className="flex items-center">
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
{isProOnly && user?.plan === "FREE" ? (
<Badge className="ml-2" variant="default">
PRO
</Badge>
) : null}
</div>
</div>
{/* reintroduce once we show permissions and features
<NavTabs tabs={tabs} linkProps={{ shallow: true }} /> */}
<h2 className="text-sm text-gray-500">
<span className="capitalize">{categories[0]}</span> • {t("published_by", { author })}
</h2>
</header>
</div>
<div className="justify-between px-8 py-10 md:flex">
<div className="prose-sm prose mb-6">{body}</div>
<div className="md:max-w-80 flex-1 md:ml-8">
<h4 className="font-medium text-gray-900 ">{t("categories")}</h4>
<div className="space-x-2">
{categories.map((category) => (
<Link href={"/apps/categories/" + category} key={category}>
<a>
<Badge variant="success">{category}</Badge>
</a>
</Link>
))}
</div>
<h4 className="mt-8 font-medium text-gray-900 ">{t("pricing")}</h4>
<small>
{price === 0 ? (
"Free"
) : (
<>
{Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && "/" + t("month")}
</>
)}
<div className="mt-4 sm:mt-0 sm:text-right">
{!isLoading ? (
isGlobal || (installedAppCount > 0 && allowedMultipleInstalls) ? (
<div className="space-x-3">
<Button StartIcon={CheckIcon} color="secondary" disabled>
{installedAppCount > 0
? t("active_install", { count: installedAppCount })
: t("globally_install")}
</Button>
<InstallAppButton
type={type}
isProOnly={isProOnly}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
onClick: () => {
mutation.mutate({ type });
},
loading: mutation.isLoading,
};
}
return (
<Button StartIcon={PlusIcon} {...props} data-testid="install-app-button">
{t("add_another")}
</Button>
);
}}
/>
</div>
) : (
<InstallAppButton
type={type}
isProOnly={isProOnly}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
onClick: () => {
mutation.mutate({ type });
},
loading: mutation.isLoading,
};
}
return (
<Button data-testid="install-app-button" {...props}>
{t("install_app")}
</Button>
);
}}
/>
)
) : (
<SkeletonButton width="24" height="10" />
)}
{price !== 0 && (
<small className="block text-right">
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
{feeType === "monthly" && "/" + t("month")}
</small>
<h4 className="mt-8 mb-2 font-medium text-gray-900 ">{t("learn_more")}</h4>
<ul className="prose -ml-1 -mr-1 text-xs leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={docs}>
<BookOpenIcon className="mr-1 -mt-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
)}
{website && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={website}>
<ExternalLinkIcon className="mr-1 -mt-px inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
)}
{email && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={"mailto:" + email}>
<MailIcon className="mr-1 -mt-px inline h-4 w-4" />
{email}
</a>
</li>
)}
{tos && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={tos}>
<DocumentTextIcon className="mr-1 -mt-px inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
)}
{privacy && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={privacy}>
<ShieldCheckIcon className="mr-1 -mt-px inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="my-6" />
<small className="leading-1 block text-gray-500">
Every app published on the Cal.com App Store is open source and thoroughly tested via peer
reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are
published by Cal.com. If you encounter inappropriate content or behaviour please report it.
</small>
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
<FlagIcon className="inline h-3 w-3" /> Report App
</a>
</div>
)}
</div>
</div>
</Shell>
</>
{/* reintroduce once we show permissions and features
<NavTabs tabs={tabs} linkProps={{ shallow: true }} /> */}
</div>
<div className="justify-between px-8 py-10 md:flex">
<div className="prose-sm prose mb-6">{body}</div>
<div className="md:max-w-80 flex-1 md:ml-8">
<h4 className="font-medium text-gray-900 ">{t("categories")}</h4>
<div className="space-x-2">
{categories.map((category) => (
<Link href={"/apps/categories/" + category} key={category}>
<a>
<Badge variant="success">{category}</Badge>
</a>
</Link>
))}
</div>
<h4 className="mt-8 font-medium text-gray-900 ">{t("pricing")}</h4>
<small>
{price === 0 ? (
"Free"
) : (
<>
{Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price)}
{feeType === "monthly" && "/" + t("month")}
</>
)}
</small>
<h4 className="mt-8 mb-2 font-medium text-gray-900 ">{t("learn_more")}</h4>
<ul className="prose -ml-1 -mr-1 text-xs leading-5">
{docs && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={docs}>
<BookOpenIcon className="mr-1 -mt-1 inline h-4 w-4" />
{t("documentation")}
</a>
</li>
)}
{website && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={website}>
<ExternalLinkIcon className="mr-1 -mt-px inline h-4 w-4" />
{website.replace("https://", "")}
</a>
</li>
)}
{email && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={"mailto:" + email}>
<MailIcon className="mr-1 -mt-px inline h-4 w-4" />
{email}
</a>
</li>
)}
{tos && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={tos}>
<DocumentTextIcon className="mr-1 -mt-px inline h-4 w-4" />
{t("terms_of_service")}
</a>
</li>
)}
{privacy && (
<li>
<a
target="_blank"
rel="noreferrer"
className="text-blue-500 no-underline hover:underline"
href={privacy}>
<ShieldCheckIcon className="mr-1 -mt-px inline h-4 w-4" />
{t("privacy_policy")}
</a>
</li>
)}
</ul>
<hr className="my-6" />
<small className="leading-1 block text-gray-500">
Every app published on the Cal.com App Store is open source and thoroughly tested via peer
reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are
published by Cal.com. If you encounter inappropriate content or behaviour please report it.
</small>
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
<FlagIcon className="inline h-3 w-3" /> Report App
</a>
</div>
</div>
</div>
);
};
export default function App(props: {
name: string;
type: AppType["type"];
isGlobal?: AppType["isGlobal"];
logo: string;
body: React.ReactNode;
categories: string[];
author: string;
pro?: boolean;
price?: number;
commission?: number;
feeType?: AppType["feeType"];
docs?: string;
website?: string;
email: string; // required
tos?: string;
privacy?: string;
licenseRequired: AppType["licenseRequired"];
isProOnly: AppType["isProOnly"];
}) {
return (
<Shell large isPublic>
{props.licenseRequired ? (
<LicenseRequired>
<Component {...props} />
</LicenseRequired>
) : (
<Component {...props} />
)}
</Shell>
);
}
+27 -49
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import Head from "next/head";
import { useBrandColors } from "@calcom/embed-core/embed-iframe";
@@ -238,54 +238,32 @@ const BrandColor = ({
? darkVal
: "#" + darkVal
: fallBackHex(darkVal, true);
useEffect(() => {
document.documentElement.style.setProperty(
"--booking-highlight-color",
embedBrandingColors.highlightColor || "#10B981" // green--500
);
document.documentElement.style.setProperty(
"--booking-lightest-color",
embedBrandingColors.lightestColor || "#E1E1E1" // gray--200
);
document.documentElement.style.setProperty(
"--booking-lighter-color",
embedBrandingColors.lighterColor || "#ACACAC" // gray--400
);
document.documentElement.style.setProperty(
"--booking-light-color",
embedBrandingColors.lightColor || "#888888" // gray--500
);
document.documentElement.style.setProperty(
"--booking-median-color",
embedBrandingColors.medianColor || "#494949" // gray--600
);
document.documentElement.style.setProperty(
"--booking-dark-color",
embedBrandingColors.darkColor || "#313131" // gray--800
);
document.documentElement.style.setProperty(
"--booking-darker-color",
embedBrandingColors.darkerColor || "#292929" // gray--900
);
document.documentElement.style.setProperty("--brand-color", lightVal);
document.documentElement.style.setProperty("--brand-text-color", getContrastingTextColor(lightVal, true));
document.documentElement.style.setProperty("--brand-color-dark-mode", darkVal);
document.documentElement.style.setProperty(
"--brand-text-color-dark-mode",
getContrastingTextColor(darkVal, true)
);
}, [
embedBrandingColors.highlightColor,
embedBrandingColors.lightestColor,
embedBrandingColors.lighterColor,
embedBrandingColors.lightColor,
embedBrandingColors.medianColor,
embedBrandingColors.darkColor,
embedBrandingColors.darkerColor,
lightVal,
darkVal,
]);
return null;
return (
<Head>
<style>
{`body {
/* green--500*/
--booking-highlight-color: ${embedBrandingColors.highlightColor || "#10B981"};
/* gray--200 */
--booking-lightest-color: ${embedBrandingColors.lightestColor || "#E1E1E1"};
/* gray--400 */
--booking-lighter-color: ${embedBrandingColors.lighterColor || "#ACACAC"};
/* gray--500 */
--booking-light-color: ${embedBrandingColors.lightColor || "#888888"};
/* gray--600 */
--booking-median-color: ${embedBrandingColors.medianColor || "#494949"};
/* gray--800 */
--booking-dark-color: ${embedBrandingColors.darkColor || "#313131"};
/* gray--900 */
--booking-darker-color: ${embedBrandingColors.darkerColor || "#292929"};
--brand-color: ${lightVal};
--brand-text-color: ${getContrastingTextColor(lightVal, true)};
--brand-color-dark-mode: ${darkVal};
--brand-text-color-dark-mode: ${getContrastingTextColor(darkVal, true)};
`}
</style>
</Head>
);
};
export default BrandColor;
@@ -61,15 +61,17 @@ const DestinationCalendarSelector = ({
query.data.connectedCalendars.map((selectedCalendar) => ({
key: selectedCalendar.credentialId,
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
options: (selectedCalendar.calendars ?? []).map((cal) => ({
label: cal.name || "",
value: `${cal.integration}:${cal.externalId}`,
})),
options: (selectedCalendar.calendars ?? [])
.filter((cal) => cal.readOnly === false)
.map((cal) => ({
label: cal.name || "",
value: `${cal.integration}:${cal.externalId}`,
})),
})) ?? [];
return (
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
<Select
name={"primarySelectedCalendar"}
name="primarySelectedCalendar"
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
options={options}
styles={{
+27 -18
View File
@@ -467,12 +467,14 @@ const tabs = [
}
return (
<>
<small className="flex py-4 text-neutral-500">{t("place_where_cal_widget_appear")}</small>
<div>
<small className="flex py-4 text-neutral-500">{t("place_where_cal_widget_appear")}</small>
</div>
<TextArea
data-testid="embed-code"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-code"
className="h-[36rem]"
className="h-[calc(100%-50px)]"
readOnly
value={
`<!-- Cal ${embedType} embed code begins -->\n` +
@@ -486,7 +488,8 @@ ${getEmbedSnippetString()}
${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previewState })}
</script>
<!-- Cal ${embedType} embed code ends -->`
}></TextArea>
}
/>
<p className="hidden text-sm text-gray-500">
{t(
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
@@ -519,7 +522,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
data-testid="embed-react"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
name="embed-react"
className="h-[36rem]"
className="h-[calc(100%-50px)]"
readOnly
value={`/* First make sure that you have installed the package */
@@ -529,7 +532,8 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
/* If you are using npm */
// npm install @calcom/embed-react
${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, previewState })}
`}></TextArea>
`}
/>
</>
);
}),
@@ -800,11 +804,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
},
});
}}>
<ArrowLeftIcon className="mr-4 w-4"></ArrowLeftIcon>
<ArrowLeftIcon className="mr-4 w-4" />
</button>
{embed.title}
</h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")}></hr>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
open={isEmbedCustomizationOpen}
@@ -916,7 +920,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
},
};
});
}}></Switch>
}}
/>
</div>
<div
className={classNames(
@@ -937,7 +942,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
});
}}
defaultValue={FloatingPopupPositionOptions[0]}
options={FloatingPopupPositionOptions}></Select>
options={FloatingPopupPositionOptions}
/>
</div>
<div
className={classNames(
@@ -958,7 +964,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
},
};
});
}}></ColorPicker>
}}
/>
</div>
</div>
<div
@@ -980,13 +987,14 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
},
};
});
}}></ColorPicker>
}}
/>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
<hr className="mt-4"></hr>
<hr className="mt-4" />
<div className="mt-4 font-medium">
<Collapsible
open={isBookingCustomizationOpen}
@@ -1020,7 +1028,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
};
});
}}
options={ThemeOptions}></Select>
options={ThemeOptions}
/>
</Label>
{[
{ name: "brandColor", title: "Brand Color" },
@@ -1063,7 +1072,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
embedType={embedType}
calLink={calLink}
previewState={previewState}
ref={refOfEmbedCodesRefs.current[tab.name]}></tab.Component>
ref={refOfEmbedCodesRefs.current[tab.name]}
/>
) : (
<tab.Component
embedType={embedType}
@@ -1073,7 +1083,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
/>
)}
</div>
<div className={router.query.tabName == "embed-preview" ? "block" : "hidden"}></div>
<div className={router.query.tabName == "embed-preview" ? "block" : "hidden"} />
</div>
<div className="mt-8 flex flex-row-reverse gap-x-2">
{tab.type === "code" ? (
@@ -1157,10 +1167,9 @@ export const EmbedButton = ({
className={className}
{...props}
data-test-eventtype-id={eventTypeId}
data-testid={"event-type-embed"}
data-testid="event-type-embed"
onClick={() => openEmbedModal()}>
<CodeIcon
className={classNames("h-4 w-4 ltr:mr-2 rtl:ml-2", dark ? "" : "text-neutral-500")}></CodeIcon>
<CodeIcon className={classNames("h-4 w-4 ltr:mr-2 rtl:ml-2", dark ? "" : "text-neutral-500")} />
{t("Embed")}
</Button>
);
+1 -1
View File
@@ -142,7 +142,7 @@ export default function ImageUploader({
</div>
)}
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
<input
onInput={onInputFile}
type="file"
+279
View File
@@ -0,0 +1,279 @@
import { SwitchVerticalIcon } from "@heroicons/react/outline";
import { SearchIcon } from "@heroicons/react/solid";
import {
KBarProvider,
KBarPortal,
KBarPositioner,
KBarAnimator,
KBarSearch,
KBarResults,
useMatches,
useKBar,
} from "kbar";
import { useRouter } from "next/router";
import { Command, CornerDownLeft } from "react-feather";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { isMac } from "@calcom/lib/isMac";
import { Tooltip } from "@calcom/ui";
type shortcutArrayType = {
shortcuts?: string[];
};
export const KBarRoot = ({ children }: { children: React.ReactNode }) => {
const router = useRouter();
// grab link to events
// quick nested actions would be extremely useful
const actions = [
// {
// id: "toggle-idle",
// name: "Test Function",
// section: "Status",
// shortcut: ["t", "f"],
// keywords: "set yourself away bookings",
// perform: () => alert("Hello World"),
// },
{
id: "workflows",
name: "Workflows",
section: "Workflows",
shortcut: ["w", "f"],
keywords: "workflows automation",
perform: () => router.push("/workflows"),
},
{
id: "event-types",
name: "Event Types",
section: "Event Types",
shortcut: ["e", "t"],
keywords: "event types",
perform: () => router.push("/event-types"),
},
{
id: "app-store",
name: "App Store",
section: "Apps",
shortcut: ["a", "s"],
keywords: "app store",
perform: () => router.push("/apps"),
},
{
id: "upcoming-bookings",
name: "Upcoming Bookings",
section: "Bookings",
shortcut: ["u", "b"],
keywords: "upcoming bookings",
perform: () => router.push("/bookings/upcoming"),
},
{
id: "recurring-bookings",
name: "Recurring Bookings",
section: "Bookings",
shortcut: ["r", "b"],
keywords: "recurring bookings",
perform: () => router.push("/bookings/recurring"),
},
{
id: "past-bookings",
name: "Past Bookings",
section: "Bookings",
shortcut: ["p", "b"],
keywords: "past bookings",
perform: () => router.push("/bookings/past"),
},
{
id: "cancelled-bookings",
name: "Cancelled Bookings",
section: "Bookings",
shortcut: ["c", "b"],
keywords: "cancelled bookings",
perform: () => router.push("/bookings/cancelled"),
},
{
id: "schedule",
name: "Schedule",
section: "Availability",
shortcut: ["s", "a"],
keywords: "schedule availability",
perform: () => router.push("/availability"),
},
{
id: "profile",
name: "Profile",
section: "Profile Settings",
shortcut: ["p", "s"],
keywords: "setting profile",
perform: () => router.push("/settings"),
},
{
id: "avatar",
name: "Change Avatar",
section: "Profile Settings",
shortcut: ["c", "a"],
keywords: "remove change modify avatar",
perform: () => router.push("/settings"),
},
{
id: "timezone",
name: "Change Timezone",
section: "Profile Settings",
shortcut: ["c", "t"],
keywords: "change modify timezone",
perform: () => router.push("/settings"),
},
{
id: "brand-color",
name: "Change Brand Color",
section: "Profile Settings",
shortcut: ["b", "c"],
keywords: "change modify brand color",
perform: () => router.push("/settings"),
},
{
id: "teams",
name: "Teams",
shortcut: ["t", "s"],
keywords: "add manage modify team",
perform: () => router.push("/settings/teams"),
},
{
id: "password",
name: "Change Password",
section: "Security Settings",
shortcut: ["c", "p"],
keywords: "change modify password",
perform: () => router.push("/settings/security"),
},
{
id: "two-factor",
name: "Two Factor Authentication",
section: "Security Settings",
shortcut: ["t", "f", "a"],
keywords: "two factor authentication",
perform: () => router.push("/settings/security"),
},
{
id: "impersonation",
name: "User Impersonation",
section: "Security Settings",
shortcut: ["u", "i"],
keywords: "user impersonation",
perform: () => router.push("/settings/security"),
},
{
id: "webhooks",
name: "Webhook",
section: "Developer Settings",
shortcut: ["w", "h"],
keywords: "webhook automation",
perform: () => router.push("/settings/developer"),
},
{
id: "api-keys",
name: "API Keys",
section: "Developer Settings",
shortcut: ["a", "p", "i"],
keywords: "api keys",
perform: () => router.push("/settings/developer"),
},
{
id: "billing",
name: "View and Manage Billing",
section: "Billing",
shortcut: ["m", "b"],
keywords: "billing view manage",
perform: () => router.push("/settings/billing"),
},
];
return <KBarProvider actions={actions}>{children}</KBarProvider>;
};
export const KBarContent = () => {
const { t } = useLocale();
return (
<KBarPortal>
<KBarPositioner>
<KBarAnimator className="z-10 w-full max-w-screen-sm overflow-hidden rounded-sm bg-white shadow-lg">
<div className="flex items-center justify-center border-b">
<SearchIcon className="mx-3 h-4 w-4 text-gray-500" />
<KBarSearch className="w-full rounded-sm py-2.5 focus-visible:outline-none" />
</div>
<RenderResults />
<div className="hidden items-center space-x-1 border-t px-2 py-1.5 text-xs text-gray-500 sm:flex">
<SwitchVerticalIcon className="h-4 w-4" /> <span className="pr-2">{t("navigate")}</span>
<CornerDownLeft className="h-4 w-4" />
<span className="pr-2">{t("open")}</span>
{isMac ? <Command className="h-3 w-3" /> : "CTRL"}
<span className="pr-1">+ K </span>
<span className="pr-2">{t("close")}</span>
</div>
</KBarAnimator>
<div className="z-1 fixed inset-0 bg-gray-600 bg-opacity-75" />
</KBarPositioner>
</KBarPortal>
);
};
export const KBarTrigger = () => {
const { query } = useKBar();
return (
<>
<Tooltip side="right" content={isMac ? "⌘ + K" : "CTRL + K"}>
<button
color="minimal"
onClick={query.toggle}
className="group flex text-sm font-medium text-neutral-500 hover:text-neutral-900">
<SearchIcon className="h-5 w-5 flex-shrink-0 text-neutral-400 group-hover:text-neutral-500" />
</button>
</Tooltip>
</>
);
};
const DisplayShortcuts = (item: shortcutArrayType) => {
const shortcuts = item.shortcuts;
return (
<span className="space-x-1">
{shortcuts?.map((shortcut) => {
return (
<kbd key={shortcut} className="rounded-sm border bg-white px-2 py-1 text-black hover:bg-gray-100">
{shortcut}
</kbd>
);
})}
</span>
);
};
function RenderResults() {
const { results } = useMatches();
console.log(results);
return (
<KBarResults
items={results}
onRender={({ item, active }) =>
typeof item === "string" ? (
<div className="bg-white p-4 text-xs uppercase text-gray-500">{item}</div>
) : (
<div
// For seeing keyboard up & down navigation in action, we need visual feedback based on "active" prop
style={{
background: active ? "rgb(245,245,245)" : "#fff",
borderLeft: active ? "2px solid black" : "2px solid transparent",
}}
className="flex items-center justify-between px-4 py-2.5 text-sm hover:cursor-pointer">
<span>{item.name}</span>
<DisplayShortcuts shortcuts={item.shortcut} />
</div>
)
}
/>
);
}
+4 -7
View File
@@ -1,18 +1,15 @@
import { LOGO_ICON, LOGO } from "@calcom/lib/constants";
export default function Logo({ small, icon }: { small?: boolean; icon?: boolean }) {
return (
<h1 className="inline">
<strong>
{icon ? (
// eslint-disable-next-line @next/next/no-img-element
<img className="mx-auto w-9" alt="Cal" title="Cal" src="/cal-com-icon-white.svg" />
<img className="mx-auto w-9" alt="Cal" title="Cal" src={LOGO_ICON} />
) : (
// eslint-disable-next-line @next/next/no-img-element
<img
className={small ? "h-4 w-auto" : "h-5 w-auto"}
alt="Cal"
title="Cal"
src="/calendso-logo-white-word.svg"
/>
<img className={small ? "h-4 w-auto" : "h-5 w-auto"} alt="Cal" title="Cal" src={LOGO} />
)}
</strong>
</h1>
+4 -2
View File
@@ -18,6 +18,7 @@ export interface NavTabProps {
tabName?: string;
icon?: SVGComponent;
adminRequired?: boolean;
className?: string;
}[];
linkProps?: Omit<LinkProps, "href">;
}
@@ -58,7 +59,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
: noop;
const Component = tab.adminRequired ? AdminRequired : Fragment;
const className = tab.className || "";
return (
<Component key={tab.name}>
<Link key={tab.name} href={href} {...linkProps}>
@@ -68,7 +69,8 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
isCurrent
? "border-neutral-900 text-neutral-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium"
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
className
)}
aria-current={isCurrent ? "page" : undefined}>
{tab.icon && (
+56
View File
@@ -0,0 +1,56 @@
// This component is abstracted from /event-types/[type] for common usecase.
import { PencilIcon } from "@heroicons/react/solid";
import { useState } from "react";
export default function PencilEdit({
value,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = () => {},
placeholder = "",
readOnly = false,
}: {
value: string;
onChange?: (value: string) => void;
placeholder?: string;
readOnly?: boolean;
}) {
const [editIcon, setEditIcon] = useState(true);
const onDivClick = !readOnly
? () => {
return setEditIcon(false);
}
: // eslint-disable-next-line @typescript-eslint/no-empty-function
() => {};
return (
<div className="group relative min-h-[28px] cursor-pointer" onClick={onDivClick}>
{editIcon ? (
<>
<h1
style={{ fontSize: 22, letterSpacing: "-0.0009em" }}
className="inline-block pl-0 text-gray-900 focus:text-black group-hover:text-gray-500">
{value}
</h1>
{!readOnly ? (
<PencilIcon className="ml-1 -mt-1 inline h-4 w-4 text-gray-700 group-hover:text-gray-500" />
) : null}
</>
) : (
<div style={{ marginBottom: -11 }}>
<input
type="text"
autoFocus
style={{ top: -6, fontSize: 22 }}
required
className="relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
placeholder={placeholder}
defaultValue={value}
onBlur={(e) => {
setEditIcon(true);
onChange(e.target.value);
}}
/>
</div>
)}
</div>
);
}
+141 -90
View File
@@ -1,4 +1,5 @@
import { SelectorIcon } from "@heroicons/react/outline";
import { CollectionIcon } from "@heroicons/react/solid";
import {
ArrowLeftIcon,
CalendarIcon,
@@ -11,6 +12,7 @@ import {
MoonIcon,
ViewGridIcon,
QuestionMarkCircleIcon,
LightningBoltIcon,
} from "@heroicons/react/solid";
import { UserPlan } from "@prisma/client";
import { SessionContextValue, signOut, useSession } from "next-auth/react";
@@ -20,6 +22,7 @@ import React, { Fragment, ReactNode, useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { WEBAPP_URL, JOIN_SLACK, ROADMAP } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import Dropdown, {
@@ -34,15 +37,15 @@ import HelpMenuItem from "@ee/components/support/HelpMenuItem";
import ErrorBoundary from "@lib/ErrorBoundary";
import classNames from "@lib/classNames";
import { WEBAPP_URL } from "@lib/config/constants";
import { shouldShowOnboarding } from "@lib/getting-started";
import useMeQuery from "@lib/hooks/useMeQuery";
import useTheme from "@lib/hooks/useTheme";
import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding";
import { KBarRoot, KBarContent, KBarTrigger } from "@components/Kbar";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";
import Badge from "@components/ui/Badge";
import ImpersonatingBanner from "@components/ui/ImpersonatingBanner";
import pkg from "../package.json";
@@ -122,6 +125,12 @@ const Layout = ({
}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => {
const isEmbed = useIsEmbed();
const router = useRouter();
const { data: routingForms } = trpc.useQuery([
"viewer.appById",
{
appId: "routing_forms",
},
]);
const { t } = useLocale();
const navigation = [
@@ -143,11 +152,26 @@ const Layout = ({
icon: ClockIcon,
current: router.asPath.startsWith("/availability"),
},
routingForms
? {
name: "Routing Forms",
href: "/apps/routing_forms/forms",
icon: CollectionIcon,
current: router.asPath.startsWith("/apps/routing_forms/"),
}
: null,
{
name: t("workflows"),
href: "/workflows",
icon: LightningBoltIcon,
current: router.asPath.startsWith("/workflows"),
pro: true,
},
{
name: t("apps"),
href: "/apps",
icon: ViewGridIcon,
current: router.asPath.startsWith("/apps"),
current: router.asPath.startsWith("/apps") && !router.asPath.startsWith("/apps/routing_forms/"),
child: [
{
name: t("app_store"),
@@ -192,11 +216,16 @@ const Layout = ({
<div className="flex w-14 flex-col lg:w-56">
<div className="flex h-0 flex-1 flex-col border-r border-gray-200 bg-white">
<div className="flex flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5">
<Link href="/event-types">
<a className="px-4 md:hidden lg:inline">
<Logo small />
</a>
</Link>
<div className="justify-between md:hidden lg:flex">
<Link href="/event-types">
<a className="px-4">
<Logo small />
</a>
</Link>
<div className="px-2">
<KBarTrigger />
</div>
</div>
{/* logo icon for tablet */}
<Link href="/event-types">
<a className="text-center md:inline lg:hidden">
@@ -204,48 +233,58 @@ const Layout = ({
</a>
</Link>
<nav className="mt-2 flex-1 space-y-1 bg-white px-2 lg:mt-5">
{navigation.map((item) => (
<Fragment key={item.name}>
<Link href={item.href}>
<a
aria-label={item.name}
className={classNames(
item.current
? "bg-neutral-100 text-neutral-900"
: "text-neutral-500 hover:bg-gray-50 hover:text-neutral-900",
"group flex items-center rounded-sm px-2 py-2 text-sm font-medium"
)}>
<item.icon
{navigation.map((item) =>
!item ? null : (
<Fragment key={item.name}>
<Link href={item.href}>
<a
aria-label={item.name}
className={classNames(
item.current
? "text-neutral-500"
: "text-neutral-400 group-hover:text-neutral-500",
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
? "bg-neutral-100 text-neutral-900"
: "text-neutral-500 hover:bg-gray-50 hover:text-neutral-900",
"group flex items-center rounded-sm px-2 py-2 text-sm font-medium"
)}>
<item.icon
className={classNames(
item.current
? "text-neutral-500"
: "text-neutral-400 group-hover:text-neutral-500",
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
)}
aria-hidden="true"
/>
<span className="hidden lg:inline">{item.name}</span>
{item.pro && (
<span className="ml-1">
{plan === "FREE" && <Badge variant="default">PRO</Badge>}
</span>
)}
aria-hidden="true"
/>
<span className="hidden lg:inline">{item.name}</span>
</a>
</Link>
{item.child &&
router.asPath.startsWith(item.href) &&
item.child.map((item) => {
return (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.current
? "text-neutral-900"
: "text-neutral-500 hover:text-neutral-900",
"group hidden items-center rounded-sm px-2 py-2 pl-10 text-sm font-medium lg:flex"
)}>
<span className="hidden lg:inline">{item.name}</span>
</a>
</Link>
);
})}
</Fragment>
))}
</a>
</Link>
{item.child &&
router.asPath.startsWith(item.href) &&
item.child.map((item) => {
return (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.current
? "text-neutral-900"
: "text-neutral-500 hover:text-neutral-900",
"group hidden items-center rounded-sm px-2 py-2 pl-10 text-sm font-medium lg:flex"
)}>
<span className="hidden lg:inline">{item.name}</span>
</a>
</Link>
);
})}
</Fragment>
)
)}
<span className="group flex items-center rounded-sm px-2 py-2 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden">
<KBarTrigger />
</span>
</nav>
</div>
<TrialBanner />
@@ -288,9 +327,12 @@ const Layout = ({
<Logo />
</a>
</Link>
<div className="flex items-center gap-3 self-center">
<div className="flex items-center gap-2 self-center">
<span className="group flex items-center rounded-full p-2.5 text-sm font-medium text-neutral-500 hover:bg-gray-50 hover:text-neutral-900 lg:hidden">
<KBarTrigger />
</span>
<button className="rounded-full bg-white p-2 text-gray-400 hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
<span className="sr-only">{t("view_notifications")}</span>
<span className="sr-only">{t("settings")}</span>
<Link href="/settings/profile">
<a>
<CogIcon className="h-6 w-6" aria-hidden="true" />
@@ -322,23 +364,21 @@ const Layout = ({
<div
className={classNames(
props.large && "bg-gray-100 py-8 lg:mb-8 lg:pt-16 lg:pb-7",
"block min-h-[80px] justify-between px-4 sm:flex sm:px-6 md:px-8"
"block justify-between px-4 sm:flex sm:px-6 md:px-8"
)}>
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
<div className="mb-8 w-full">
{props.isLoading ? (
<>
<div className="mb-1 h-6 w-24 animate-pulse rounded-md bg-gray-200"></div>
<div className="mb-1 h-6 w-32 animate-pulse rounded-md bg-gray-200"></div>
<div className="mb-1 h-6 w-24 animate-pulse rounded-md bg-gray-200" />
<div className="mb-1 h-6 w-32 animate-pulse rounded-md bg-gray-200" />
</>
) : (
<>
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
{props.heading}
</h1>
<p className="min-h-10 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">
{props.subtitle}
</p>
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
</>
)}
</div>
@@ -358,8 +398,11 @@ const Layout = ({
style={isEmbed ? { display: "none" } : {}}
className="bottom-nav fixed bottom-0 z-30 flex w-full bg-white shadow md:hidden">
{/* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */}
{navigation.flatMap((item, itemIdx) =>
item.href === "/settings/profile" ? (
{navigation.flatMap((item, itemIdx) => {
if (!item) {
return null;
}
return item.href === "/settings/profile" ? (
[]
) : (
<Link key={item.name} href={item.href}>
@@ -378,11 +421,11 @@ const Layout = ({
)}
aria-hidden="true"
/>
<span className="truncate">{item.name}</span>
<span className="block truncate">{item.name}</span>
</a>
</Link>
)
)}
);
})}
</nav>
)}
{/* add padding to content for mobile navigation*/}
@@ -417,7 +460,6 @@ type LayoutProps = {
export default function Shell(props: LayoutProps) {
const { loading, session } = useRedirectToLoginIfUnauthenticated(props.isPublic);
const { isRedirectingToOnboarding } = useRedirectToOnboardingIfNeeded();
const { isReady, Theme } = useTheme("light");
const query = useMeQuery();
const user = query.data;
@@ -425,14 +467,11 @@ export default function Shell(props: LayoutProps) {
const i18n = useViewerI18n();
const { status } = useSession();
const isLoading =
i18n.status === "loading" ||
query.status === "loading" ||
isRedirectingToOnboarding ||
loading ||
!isReady;
const isLoading = isRedirectingToOnboarding || loading;
if (isLoading) {
// Don't show any content till translations are loaded.
// As they are cached infintely, this status would be loading just once for the app's lifetime until refresh
if (i18n.status === "loading") {
return (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
<Loader />
@@ -443,11 +482,11 @@ export default function Shell(props: LayoutProps) {
if (!session && !props.isPublic) return null;
return (
<>
<Theme />
<KBarRoot>
<CustomBranding lightVal={user?.brandColor} darkVal={user?.darkBrandColor} />
<MemoizedLayout plan={user?.plan} status={status} {...props} isLoading={isLoading} />
</>
<KBarContent />
</KBarRoot>
);
}
@@ -455,6 +494,7 @@ function UserDropdown({ small }: { small?: boolean }) {
const { t } = useLocale();
const query = useMeQuery();
const user = query.data;
const mutation = trpc.useMutation("viewer.away", {
onSettled() {
utils.invalidateQueries("viewer.me");
@@ -463,12 +503,19 @@ function UserDropdown({ small }: { small?: boolean }) {
const utils = trpc.useContext();
const [helpOpen, setHelpOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
if (!user) {
return null;
}
const onHelpItemSelect = () => {
setHelpOpen(false);
setMenuOpen(false);
};
// Prevent rendering dropdown if user isn't available.
// We don't want to show nameless user.
if (!user) {
return null;
}
return (
<Dropdown open={menuOpen} onOpenChange={() => setHelpOpen(false)}>
<DropdownMenuTrigger asChild onClick={() => setMenuOpen(true)}>
@@ -482,25 +529,25 @@ function UserDropdown({ small }: { small?: boolean }) {
// eslint-disable-next-line @next/next/no-img-element
<img
className="rounded-full"
src={WEBAPP_URL + "/" + user?.username + "/avatar.png"}
alt={user?.username || "Nameless User"}
src={WEBAPP_URL + "/" + user.username + "/avatar.png"}
alt={user.username || "Nameless User"}
/>
}
{!user?.away && (
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-green-500"></div>
{!user.away && (
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-green-500" />
)}
{user?.away && (
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-yellow-500"></div>
{user.away && (
<div className="absolute bottom-0 right-0 h-3 w-3 rounded-full border-2 border-white bg-yellow-500" />
)}
</span>
{!small && (
<span className="flex flex-grow items-center truncate">
<span className="flex-grow truncate text-sm">
<span className="block truncate font-medium text-gray-900">
{user?.name || "Nameless User"}
{user.name || "Nameless User"}
</span>
<span className="block truncate font-normal text-neutral-500">
{user?.username
{user.username
? process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com"
? `cal.com/${user.username}`
: `/${user.username}`
@@ -523,24 +570,24 @@ function UserDropdown({ small }: { small?: boolean }) {
<DropdownMenuItem>
<a
onClick={() => {
mutation.mutate({ away: !user?.away });
mutation.mutate({ away: user?.away });
utils.invalidateQueries("viewer.me");
}}
className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
<MoonIcon
className={classNames(
user?.away
user.away
? "text-purple-500 group-hover:text-purple-700"
: "text-gray-500 group-hover:text-gray-700",
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
)}
aria-hidden="true"
/>
{user?.away ? t("set_as_free") : t("set_as_away")}
{user.away ? t("set_as_free") : t("set_as_away")}
</a>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
{user?.username && (
{user.username && (
<DropdownMenuItem>
<a
target="_blank"
@@ -555,7 +602,7 @@ function UserDropdown({ small }: { small?: boolean }) {
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<a
href="https://cal.com/slack"
href={JOIN_SLACK}
target="_blank"
rel="noreferrer"
className="flex px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900">
@@ -569,16 +616,20 @@ function UserDropdown({ small }: { small?: boolean }) {
<g clipRule="evenodd" fillRule="evenodd">
<path
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
fill="currentColor"></path>
fill="currentColor"
/>
<path
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
fill="currentColor"></path>
fill="currentColor"
/>
<path
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
fill="currentColor"></path>
fill="currentColor"
/>
<path
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
fill="currentColor"></path>
fill="currentColor"
/>
</g>
</svg>
{t("join_our_slack")}
@@ -588,7 +639,7 @@ function UserDropdown({ small }: { small?: boolean }) {
<a
target="_blank"
rel="noopener noreferrer"
href="https://cal.com/roadmap"
href={ROADMAP}
className="flex items-center px-4 py-2 text-sm text-gray-700">
<MapIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("visit_roadmap")}
</a>
+3 -2
View File
@@ -9,7 +9,7 @@ export type SwatchProps = {
const Swatch = (props: SwatchProps) => {
const { size, backgroundColor, onClick } = props;
return (
<div className="border-2 border-gray-200 p-1 shadow-sm">
<div className="border-2 border-gray-200 p-1">
<div
onClick={onClick}
style={{ backgroundColor }}
@@ -18,7 +18,8 @@ const Swatch = (props: SwatchProps) => {
size === "sm" && "h-6 w-6 rounded-sm",
size === "base" && "h-16 w-16 rounded-sm",
size === "lg" && "h-24 w-24 rounded-sm"
)}></div>
)}
/>
</div>
);
};
+1
View File
@@ -19,6 +19,7 @@ export default function AllApps({ apps }: { apps: App[] }) {
logo={app.logo}
rating={app.rating}
reviews={app.reviews}
isProOnly={app.isProOnly}
/>
))}
</div>
+14 -1
View File
@@ -2,6 +2,10 @@ import Link from "next/link";
import Button from "@calcom/ui/Button";
import { trpc } from "@lib/trpc";
import Badge from "@components/ui/Badge";
interface AppCardProps {
logo: string;
name: string;
@@ -10,9 +14,11 @@ interface AppCardProps {
description: string;
rating: number;
reviews?: number;
isProOnly?: boolean;
}
export default function AppCard(props: AppCardProps) {
const { data: user } = trpc.useQuery(["viewer.me"]);
return (
<Link href={"/apps/" + props.slug}>
<a
@@ -32,7 +38,14 @@ export default function AppCard(props: AppCardProps) {
Add
</Button>
</div>
<h3 className="font-medium">{props.name}</h3>
<div className="flex items-center">
<h3 className="font-medium">{props.name}</h3>
{props.isProOnly && user?.plan === "FREE" ? (
<Badge className="ml-2" variant="default">
PRO
</Badge>
) : null}
</div>
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
+5 -5
View File
@@ -7,7 +7,7 @@ import { ShellSubHeading } from "@components/Shell";
function SkeletonLoader({ className }: { className?: string }) {
return (
<>
<ShellSubHeading title={<div className="h-6 w-32 rounded-sm bg-gray-100"></div>} {...{ className }} />
<ShellSubHeading title={<div className="h-6 w-32 rounded-sm bg-gray-100" />} {...{ className }} />
<ul className="-mx-4 animate-pulse divide-y divide-neutral-200 rounded-sm border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
@@ -24,16 +24,16 @@ function SkeletonItem() {
<li className="group flex w-full items-center justify-between p-3">
<div className="flex-grow truncate text-sm">
<div className="flex justify-start space-x-2">
<SkeletonText width="10" height="10"></SkeletonText>
<SkeletonText width="10" height="10" />
<div className="space-y-2">
<SkeletonText height="4" width="32"></SkeletonText>
<SkeletonText height="4" width="16"></SkeletonText>
<SkeletonText height="4" width="32" />
<SkeletonText height="4" width="16" />
</div>
</div>
</div>
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
<SkeletonText width="32" height="11"></SkeletonText>
<SkeletonText width="32" height="11" />
</div>
</div>
</li>
@@ -30,6 +30,7 @@ const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
logo={app.logo}
rating={app.rating}
reviews={app.reviews}
isProOnly={app.isProOnly}
/>
)}
/>
+1 -1
View File
@@ -34,7 +34,7 @@ export default function SAMLLogin(props: Props) {
<div className="mt-5">
<Button
color="secondary"
data-testid={"saml"}
data-testid="saml"
className="flex w-full justify-center"
onClick={async (event) => {
event.preventDefault();
@@ -65,7 +65,7 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
type="text"
id="name"
required
className="block w-full rounded-sm border-gray-300 text-sm shadow-sm"
className="block w-full rounded-sm border-gray-300 text-sm"
placeholder={t("default_schedule_name")}
{...register("name")}
/>
@@ -1,13 +1,11 @@
import { PlusIcon, TrashIcon } from "@heroicons/react/outline";
import { DuplicateIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import dayjs, { Dayjs, ConfigType } from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { GroupBase, Props } from "react-select";
import dayjs, { Dayjs, ConfigType } from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import Dropdown, { DropdownMenuTrigger, DropdownMenuContent } from "@calcom/ui/Dropdown";
@@ -19,9 +17,6 @@ import { TimeRange } from "@lib/types/schedule";
import Select from "@components/ui/form/Select";
dayjs.extend(utc);
dayjs.extend(timezone);
/** Begin Time Increments For Select */
const increment = 15;
@@ -19,13 +19,13 @@ function SkeletonItem() {
<li className="group flex w-full items-center justify-between px-2 py-[23px] sm:px-6">
<div className="flex-grow truncate text-sm">
<div className="flex flex-col space-y-2">
<SkeletonText width="32" height="4"></SkeletonText>
<SkeletonText width="32" height="2"></SkeletonText>
<SkeletonText width="32" height="4" />
<SkeletonText width="32" height="2" />
</div>
</div>
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
<SkeletonText width="12" height="6"></SkeletonText>
<SkeletonText width="12" height="6" />
</div>
</div>
</li>
@@ -37,8 +37,8 @@ export const AvailabilitySelectSkeletonLoader = () => {
<li className="group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3">
<div className="flex-grow truncate text-sm">
<div className="flex justify-between">
<SkeletonText width="32" height="4"></SkeletonText>
<SkeletonText width="4" height="4"></SkeletonText>
<SkeletonText width="32" height="4" />
<SkeletonText width="4" height="4" />
</div>
</div>
</li>
@@ -1,9 +1,9 @@
import { SchedulingType } from "@prisma/client";
import dayjs, { Dayjs } from "dayjs";
import Link from "next/link";
import { useRouter } from "next/router";
import { FC, useEffect, useState } from "react";
import dayjs, { Dayjs } from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { nameOfDay } from "@calcom/lib/weekday";
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
@@ -102,10 +102,10 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
)}>
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
{!!seatsPerTimeSlot && <p className={`text-sm`}>{t("booking_full")}</p>}
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
</div>
) : (
<Link href={bookingUrl}>
<Link href={bookingUrl} prefetch={false}>
<a
className={classNames(
"text-primary-500 hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 block rounded-sm border bg-white py-4 font-medium hover:text-white dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black",
@@ -9,11 +9,11 @@ import {
} from "@heroicons/react/outline";
import { RefreshIcon } from "@heroicons/react/solid";
import { BookingStatus } from "@prisma/client";
import dayjs from "dayjs";
import { useRouter } from "next/router";
import { useState } from "react";
import { useMutation } from "react-query";
import dayjs from "@calcom/dayjs";
import { parseRecurringEvent } from "@calcom/lib";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -206,6 +206,7 @@ function BookingListItem(booking: BookingItemProps) {
[recurringStrings, recurringDates] = parseRecurringDates(
{
startDate: booking.startTime,
timeZone: user?.timeZone,
recurringEvent: parseRecurringEvent(booking.eventType.recurringEvent),
recurringCount: booking.recurringCount,
},
-269
View File
@@ -1,269 +0,0 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
import { PeriodType } from "@prisma/client";
import dayjs, { Dayjs } from "dayjs";
import dayjsBusinessTime from "dayjs-business-days2";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { memoize } from "lodash";
import { useEffect, useRef, useState } from "react";
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@lib/classNames";
import { timeZone } from "@lib/clock";
import { weekdayNames } from "@lib/core/i18n/weekday";
import { doWorkAsync } from "@lib/doWorkAsync";
import isOutOfBounds from "@lib/isOutOfBounds";
import getSlots from "@lib/slots";
import { WorkingHours } from "@lib/types/schedule";
import Loader from "@components/Loader";
dayjs.extend(dayjsBusinessTime);
dayjs.extend(utc);
dayjs.extend(timezone);
type DatePickerProps = {
weekStart: string;
onDatePicked: (pickedDate: Dayjs) => void;
workingHours: WorkingHours[];
eventLength: number;
date: Dayjs | null;
periodType: PeriodType;
periodStartDate: Date | null;
periodEndDate: Date | null;
periodDays: number | null;
periodCountCalendarDays: boolean | null;
minimumBookingNotice: number;
};
function DatePicker({
weekStart,
onDatePicked,
workingHours,
eventLength,
date,
periodType = PeriodType.UNLIMITED,
periodStartDate,
periodEndDate,
periodDays,
periodCountCalendarDays,
minimumBookingNotice,
}: DatePickerProps): JSX.Element {
const { i18n } = useLocale();
const [browsingDate, setBrowsingDate] = useState<Dayjs | null>(date);
const enabledDateButtonEmbedStyles = useEmbedStyles("enabledDateButton");
const disabledDateButtonEmbedStyles = useEmbedStyles("disabledDateButton");
const [month, setMonth] = useState<string>("");
const [year, setYear] = useState<string>("");
const [isFirstMonth, setIsFirstMonth] = useState<boolean>(false);
const [daysFromState, setDays] = useState<
| {
disabled: boolean;
date: number;
}[]
| null
>(null);
useEffect(() => {
if (!browsingDate || (date && browsingDate.utcOffset() !== date?.utcOffset())) {
setBrowsingDate(date || dayjs().tz(timeZone()));
}
}, [date, browsingDate]);
useEffect(() => {
if (browsingDate) {
setMonth(browsingDate.toDate().toLocaleString(i18n.language, { month: "long" }));
setYear(browsingDate.format("YYYY"));
setIsFirstMonth(browsingDate.startOf("month").isBefore(dayjs()));
setDays(null);
}
}, [browsingDate, i18n.language]);
const isDisabled = (
day: number,
{
browsingDate,
periodType,
periodStartDate,
periodEndDate,
periodCountCalendarDays,
periodDays,
eventLength,
minimumBookingNotice,
workingHours,
}: Omit<DatePickerProps, "weekStart" | "onDatePicked" | "date"> & {
browsingDate: Dayjs;
}
) => {
const date = browsingDate.startOf("day").date(day);
return (
isOutOfBounds(date, {
periodType,
periodStartDate,
periodEndDate,
periodCountCalendarDays,
periodDays,
}) ||
!getSlots({
inviteeDate: date,
frequency: eventLength,
minimumBookingNotice,
workingHours,
eventLength,
}).length
);
};
const isDisabledRef = useRef(
memoize(isDisabled, (day, { browsingDate }) => {
// Make a composite cache key
return day + "_" + browsingDate.toString();
})
);
const days = (() => {
if (!browsingDate) {
return [];
}
if (daysFromState) {
return daysFromState;
}
// Create placeholder elements for empty days in first week
let weekdayOfFirst = browsingDate.date(1).day();
if (weekStart === "Monday") {
weekdayOfFirst -= 1;
if (weekdayOfFirst < 0) weekdayOfFirst = 6;
}
const days = Array(weekdayOfFirst).fill(null);
const isDisabledMemoized = isDisabledRef.current;
const daysInMonth = browsingDate.daysInMonth();
const daysInitialOffset = days.length;
// Build UI with All dates disabled
for (let i = 1; i <= daysInMonth; i++) {
days.push({
disabled: true,
date: i,
});
}
// Update dates with their availability
doWorkAsync({
batch: 1,
name: "DatePicker",
length: daysInMonth,
callback: (i: number) => {
const day = i + 1;
days[daysInitialOffset + i] = {
disabled: isDisabledMemoized(day, {
browsingDate,
periodType,
periodStartDate,
periodEndDate,
periodCountCalendarDays,
periodDays,
eventLength,
minimumBookingNotice,
workingHours,
}),
date: day,
};
},
batchDone: () => {
setDays([...days]);
},
});
return days;
// eslint-disable-next-line react-hooks/exhaustive-deps
})();
if (!browsingDate) {
return <Loader />;
}
// Handle month changes
const incrementMonth = () => {
setBrowsingDate(browsingDate?.add(1, "month"));
};
const decrementMonth = () => {
setBrowsingDate(browsingDate?.subtract(1, "month"));
};
return (
<div
className={
"mt-8 sm:mt-0 sm:min-w-[455px] " +
(date
? "w-full sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 "
: "w-full sm:pl-4")
}>
<div className="mb-4 flex text-xl font-light">
<span className="w-1/2 dark:text-white">
<strong className="text-bookingdarker dark:text-white">{month}</strong>{" "}
<span className="text-bookinglight">{year}</span>
</span>
<div className="w-1/2 text-right dark:text-gray-400">
<button
onClick={decrementMonth}
className={classNames(
"group p-1 ltr:mr-2 rtl:ml-2",
isFirstMonth && "text-bookinglighter dark:text-gray-600"
)}
disabled={isFirstMonth}
data-testid="decrementMonth">
<ChevronLeftIcon className="h-5 w-5 group-hover:text-black dark:group-hover:text-white" />
</button>
<button className="group p-1" onClick={incrementMonth} data-testid="incrementMonth">
<ChevronRightIcon className="h-5 w-5 group-hover:text-black dark:group-hover:text-white" />
</button>
</div>
</div>
<div className="border-bookinglightest grid grid-cols-7 gap-4 border-t border-b text-center dark:border-gray-800 sm:border-0">
{weekdayNames(i18n.language, weekStart === "Sunday" ? 0 : 1, "short").map((weekDay) => (
<div key={weekDay} className="text-bookinglight my-4 text-xs uppercase tracking-widest">
{weekDay}
</div>
))}
</div>
<div className="grid grid-cols-7 gap-2 text-center">
{days.map((day, idx) => (
<div
key={day === null ? `e-${idx}` : `day-${day.date}`}
style={{
paddingTop: "100%",
}}
className="relative w-full">
{day === null ? (
<div key={`e-${idx}`} />
) : (
<button
onClick={() => onDatePicked(browsingDate.date(day.date))}
disabled={day.disabled}
style={
day.disabled ? { ...disabledDateButtonEmbedStyles } : { ...enabledDateButtonEmbedStyles }
}
className={classNames(
"hover:border-brand disabled:text-bookinglighter absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-sm text-center font-medium hover:border disabled:cursor-default disabled:font-light disabled:hover:border-0 dark:hover:border-white",
date && date.isSame(browsingDate.date(day.date), "day")
? "bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast disabled:bg-gray-100 disabled:dark:bg-gray-600 disabled:dark:text-white"
: ""
)}
data-testid="day"
data-disabled={day.disabled}>
{day.date}
</button>
)}
</div>
))}
</div>
</div>
);
}
export default DatePicker;
+1 -1
View File
@@ -53,7 +53,7 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone }) => {
id="timeZone"
value={selectedTimeZone}
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm"
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 focus:ring-black sm:text-sm"
/>
</div>
) : null;
@@ -15,17 +15,15 @@ import {
import { EventType } from "@prisma/client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useContracts } from "contexts/contractsContext";
import dayjs, { Dayjs } from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import timeZone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { TFunction } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { z } from "zod";
import { AppStoreLocationType, LocationObject, LocationType } from "@calcom/app-store/locations";
import dayjs, { Dayjs } from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
useEmbedStyles,
@@ -33,14 +31,12 @@ import {
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import classNames from "@calcom/lib/classNames";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { yyyymmdd } from "@calcom/lib/date-fns";
import { CAL_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { localStorage } from "@calcom/lib/webstorage";
import DatePicker from "@calcom/ui/booker/DatePicker";
import { asStringOrUndefined } from "@lib/asStringOrNull";
import { timeZone as localStorageTimeZone } from "@lib/clock";
// import { timeZone } from "@lib/clock";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
@@ -61,10 +57,6 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
dayjs.extend(utc);
dayjs.extend(timeZone);
dayjs.extend(customParseFormat);
type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
export const locationKeyToString = (location: LocationObject, t: TFunction) => {
@@ -96,23 +88,19 @@ export const locationKeyToString = (location: LocationObject, t: TFunction) => {
}
};
const GoBackToPreviousPage = ({ slug }: { slug: string }) => {
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
const router = useRouter();
const [previousPage, setPreviousPage] = useState<string>();
useEffect(() => {
setPreviousPage(document.referrer);
}, []);
return previousPage === `${WEBAPP_URL}/${slug}` ? (
const path = router.asPath.split("/");
path.pop(); // Remove the last item (where we currently are)
path.shift(); // Removes first item e.g. if we were visitng "/teams/test/30mins" the array will new look like ["teams","test"]
const slug = path.join("/");
return (
<div className="flex h-full flex-col justify-end">
<ArrowLeftIcon
className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white"
onClick={() => router.back()}
/>
<p className="sr-only">Go Back</p>
<button title={t("profile")} onClick={() => router.replace(`${WEBSITE_URL}/${slug}`)}>
<ArrowLeftIcon className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white" />
<p className="sr-only">Go Back</p>
</button>
</div>
) : (
<></>
);
};
@@ -125,16 +113,16 @@ const useSlots = ({
eventTypeId: number;
startTime?: Dayjs;
endTime?: Dayjs;
timeZone: string;
timeZone?: string;
}) => {
const { data, isLoading } = trpc.useQuery(
const { data, isLoading, isIdle } = trpc.useQuery(
[
"viewer.public.slots.getSchedule",
{
eventTypeId,
startTime: startTime?.toISOString() || "",
timeZone,
endTime: endTime?.toISOString() || "",
timeZone,
},
],
{ enabled: !!startTime && !!endTime }
@@ -148,12 +136,12 @@ const useSlots = ({
}
}, [data]);
return { slots: cachedSlots, isLoading };
// The very first time isIdle is set if auto-fetch is disabled, so isIdle should also be considered a loading state.
return { slots: cachedSlots, isLoading: isLoading || isIdle };
};
const SlotPicker = ({
eventType,
timezoneDropdown,
timeFormat,
timeZone,
recurringEventCount,
@@ -161,24 +149,20 @@ const SlotPicker = ({
weekStart = 0,
}: {
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
timezoneDropdown: JSX.Element;
timeFormat: string;
timeZone: string;
timeZone?: string;
seatsPerTimeSlot?: number;
recurringEventCount?: number;
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
}) => {
const [selectedDate, setSelectedDate] = useState<Dayjs>();
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter();
useEffect(() => {
if (!router.isReady) {
return;
}
const month = asStringOrUndefined(router.query.month);
const date = asStringOrUndefined(router.query.date);
if (!router.isReady) return;
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
if (timeZone === "Etc/GMT") {
@@ -192,14 +176,13 @@ const SlotPicker = ({
setSelectedDate(dayjs(date).tz(timeZone, true));
}
}
}, [router.isReady, router.query.month, router.query.date, timeZone]);
}, [router.isReady, month, date, timeZone]);
const { i18n, isLocaleReady } = useLocale();
const { slots: _1 } = useSlots({
eventTypeId: eventType.id,
startTime: selectedDate?.startOf("month"),
endTime: selectedDate?.endOf("month"),
startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"),
timeZone,
});
const { slots: _2, isLoading } = useSlots({
@@ -215,38 +198,18 @@ const SlotPicker = ({
<>
<DatePicker
isLoading={isLoading}
className={
"mt-8 w-full sm:mt-0 sm:min-w-[455px] " +
(selectedDate
? "sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 "
: "sm:pl-4")
}
className={classNames(
"mt-8 w-full sm:mt-0 sm:min-w-[455px]",
selectedDate ? "sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 " : "sm:pl-4"
)}
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
locale={isLocaleReady ? i18n.language : "en"}
selected={selectedDate}
onChange={(selectedDate) => {
router.replace(
{
query: {
...router.query,
date: selectedDate.format("YYYY-MM-DD"),
},
},
undefined,
{ shallow: true }
);
onChange={(newDate) => {
setDate(newDate.format("YYYY-MM-DD"));
}}
onMonthChange={(browsingDate) => {
router.replace(
{
query: {
...router.query,
month: browsingDate.format("YYYY-MM"),
},
},
undefined,
{ shallow: true }
);
onMonthChange={(newMonth) => {
setMonth(newMonth.format("YYYY-MM"));
}}
browsingDate={browsingDate}
weekStart={weekStart}
@@ -255,7 +218,7 @@ const SlotPicker = ({
{selectedDate && (
<AvailableTimes
isLoading={isLoading}
slots={slots[yyyymmdd(selectedDate.toDate())]}
slots={slots[selectedDate.format("YYYY-MM-DD")]}
date={selectedDate}
timeFormat={timeFormat}
eventTypeId={eventType.id}
@@ -299,13 +262,15 @@ function TimezoneDropdown({
return (
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
<Collapsible.Trigger className="min-w-32 text-bookinglight mb-1 -ml-2 px-2 py-1 text-left dark:text-white">
<GlobeIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{timeZone || dayjs.tz.guess()}
{isTimeOptionsOpen ? (
<ChevronUpIcon className="ml-1 -mt-1 inline-block h-4 w-4" />
) : (
<ChevronDownIcon className="ml-1 -mt-1 inline-block h-4 w-4" />
)}
<p className="text-gray-600 dark:text-white">
<GlobeIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{timeZone}
{isTimeOptionsOpen ? (
<ChevronUpIcon className="ml-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
) : (
<ChevronDownIcon className="ml-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
)}
</p>
</Collapsible.Trigger>
<Collapsible.Content>
<TimeOptions onSelectTimeZone={handleSelectTimeZone} onToggle24hClock={handleToggle24hClock} />
@@ -320,6 +285,19 @@ const dateQuerySchema = z.object({
timeZone: z.string().optional().default(""),
});
const useRouterQuery = <T extends string>(name: T) => {
const router = useRouter();
const query = z.object({ [name]: z.string().optional() }).parse(router.query);
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
};
return { [name]: query[name], setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
};
const AvailabilityPage = ({ profile, eventType }: Props) => {
const router = useRouter();
const isEmbed = useIsEmbed();
@@ -378,7 +356,6 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
: recurringEventCount
? "max-w-4xl"
: "max-w-3xl";
const timezoneDropdown = useMemo(
() => (
<TimezoneDropdown
@@ -389,6 +366,9 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
),
[timeZone]
);
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
const slug = rawSlug.join("/");
return (
<>
@@ -397,7 +377,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
name={profile.name || undefined}
username={profile.slug || undefined}
username={slug || undefined}
/>
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div>
@@ -513,7 +493,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
type="number"
min="1"
max={eventType.recurringEvent.count}
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
defaultValue={eventType.recurringEvent.count}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
@@ -644,7 +624,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
type="number"
min="1"
max={eventType.recurringEvent.count}
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
defaultValue={eventType.recurringEvent.count}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
@@ -673,7 +653,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
{timezoneDropdown}
</div>
<GoBackToPreviousPage slug={profile.slug || ""} />
{!isEmbed && <GoBackToPreviousPage t={t} />}
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div>
@@ -689,29 +669,20 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
</div>
)*/}
</div>
{timeZone && (
<SlotPicker
weekStart={
typeof profile.weekStart === "string"
? ([
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
].indexOf(profile.weekStart) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
: profile.weekStart /* Allows providing weekStart as number */
}
eventType={eventType}
timezoneDropdown={timezoneDropdown}
timeFormat={timeFormat}
timeZone={timeZone}
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
recurringEventCount={recurringEventCount}
/>
)}
<SlotPicker
weekStart={
typeof profile.weekStart === "string"
? (["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
profile.weekStart
) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
: profile.weekStart /* Allows providing weekStart as number */
}
eventType={eventType}
timeFormat={timeFormat}
timeZone={timeZone}
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
recurringEventCount={recurringEventCount}
/>
</div>
</div>
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
@@ -9,9 +9,9 @@ import {
RefreshIcon,
} from "@heroicons/react/solid";
import { zodResolver } from "@hookform/resolvers/zod";
import { EventTypeCustomInputType } from "@prisma/client";
import { EventTypeCustomInputType, WorkflowActions } from "@prisma/client";
import { useContracts } from "contexts/contractsContext";
import dayjs from "dayjs";
import { isValidPhoneNumber } from "libphonenumber-js";
import { useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import Head from "next/head";
@@ -24,6 +24,7 @@ import { useMutation } from "react-query";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
useIsBackgroundTransparent,
@@ -87,6 +88,7 @@ type BookingFormValues = {
[key: string]: string | boolean;
};
rescheduleReason?: string;
smsReminderNumber?: string;
};
const BookingPage = ({
@@ -278,6 +280,14 @@ const BookingPage = ({
.object({
name: z.string().min(1),
email: z.string().email(),
phone: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
smsReminderNumber: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
})
.passthrough();
@@ -316,6 +326,15 @@ const BookingPage = ({
case LocationType.UserPhone: {
return locationInfo(locationType)?.hostPhoneNumber || "";
}
case LocationType.Around: {
return locationInfo(locationType)?.link || "";
}
case LocationType.Riverside: {
return locationInfo(locationType)?.link || "";
}
case LocationType.Whereby: {
return locationInfo(locationType)?.link || "";
}
// Catches all other location types, such as Google Meet, Zoom etc.
default:
return selectedLocation || "";
@@ -329,6 +348,7 @@ const BookingPage = ({
[recurringStrings, recurringDates] = parseRecurringDates(
{
startDate: date,
timeZone: timeZone(),
recurringEvent: eventType.recurringEvent,
recurringCount: parseInt(recurringEventCount.toString()),
},
@@ -390,6 +410,8 @@ const BookingPage = ({
})),
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
}));
recurringMutation.mutate(recurringBookings);
} else {
@@ -415,6 +437,8 @@ const BookingPage = ({
})),
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
});
}
};
@@ -422,7 +446,22 @@ const BookingPage = ({
const disableInput = !!rescheduleUid;
const disabledExceptForOwner = disableInput && !loggedInIsOwner;
const inputClassName =
"focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm";
"focus:border-brand block w-full rounded-sm border-gray-300 focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm";
let isSmsReminderNumberNeeded = false;
if (eventType.workflows.length > 0) {
eventType.workflows.forEach((workflowReference) => {
if (workflowReference.workflow.steps.length > 0) {
workflowReference.workflow.steps.forEach((step) => {
if (step.action === WorkflowActions.SMS_ATTENDEE) {
isSmsReminderNumberNeeded = true;
return;
}
});
}
});
}
return (
<div>
@@ -462,10 +501,13 @@ const BookingPage = ({
<AvatarGroup
border="border-2 border-white dark:border-gray-800"
size={14}
items={[{ image: profile.image || "", alt: profile.name || "" }].concat(
items={[
{ image: profile.image || "", alt: profile.name || "", title: profile.name || "" },
].concat(
eventType.users
.filter((user) => user.name !== profile.name)
.map((user) => ({
title: user.name || "",
image: user.avatar || "",
alt: user.name || "",
}))
@@ -656,6 +698,12 @@ const BookingPage = ({
disabled={disableInput}
/>
</div>
{bookingForm.formState.errors.phone && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<ExclamationCircleIcon className="mr-2 h-3 w-3" />
<p>{t("invalid_number")}</p>
</div>
)}
</div>
)}
{eventType.customInputs
@@ -791,6 +839,31 @@ const BookingPage = ({
)}
</div>
)}
{isSmsReminderNumberNeeded && selectedLocation !== LocationType.Phone && (
<div className="mb-4">
<label
htmlFor="smsReminderNumber"
className="block text-sm font-medium text-gray-700 dark:text-white">
{t("number_for_sms_reminders")}
</label>
<div className="mt-1">
<PhoneInput<BookingFormValues>
control={bookingForm.control}
name="smsReminderNumber"
placeholder={t("enter_phone_number")}
id="smsReminderNumber"
required
disabled={disableInput}
/>
</div>
{bookingForm.formState.errors.smsReminderNumber && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<ExclamationCircleIcon className="mr-2 h-3 w-3" />
<p>{t("invalid_number")}</p>
</div>
)}
</div>
)}
<div className="mb-4">
<label
htmlFor="notes"
@@ -84,17 +84,17 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
selection?.value === LocationType.Whereby
? z
.string()
.regex(/^http(s)?:\/\/www.whereby.com\/[a-zA-Z0-9]*/)
.regex(/^http(s)?:\/\/(www\.)?whereby.com\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Around
? z
.string()
.regex(/^http(s)?:\/\/www.around.co\/[a-zA-Z0-9]*/)
.regex(/^http(s)?:\/\/(www\.)?around.co\/[a-zA-Z0-9]*/)
.optional()
: selection?.value === LocationType.Riverside
? z
.string()
.regex(/^http(s)?:\/\/www.riverside.fm\/studio\/[a-zA-Z0-9]*/)
.regex(/^http(s)?:\/\/(www\.)?riverside.fm\/studio\/[a-zA-Z0-9]*/)
.optional()
: z.string().url().optional(),
displayLocationPublicly: z.boolean().optional(),
@@ -154,7 +154,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}></CheckboxField>
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
@@ -200,7 +201,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
: undefined
}
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
informationIconText={t("display_location_info_badge")}></CheckboxField>
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
@@ -228,7 +230,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
}
/>
{locationFormMethods.formState.errors.locationPhoneNumber && (
<p className="mt-1 text-sm text-red-500">Invalid input</p>
<p className="mt-1 text-sm text-red-500">{t("invalid_number")}</p>
)}
</div>
</div>
@@ -245,7 +247,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
id="wherebylink"
placeholder="https://www.whereby.com/cal"
required
className={"block w-full rounded-sm border-gray-300 text-sm"}
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
@@ -272,7 +274,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}></CheckboxField>
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
@@ -290,9 +293,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
type="text"
{...locationFormMethods.register("locationLink")}
id="aroundlink"
placeholder="https://www.around.com/rick"
placeholder="https://www.around.co/rick"
required
className={"block w-full rounded-sm border-gray-300 text-sm"}
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
@@ -319,7 +322,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}></CheckboxField>
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
@@ -330,7 +334,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<>
<div>
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
{t("set_around_link")}
{t("set_riverside_link")}
</label>
<div className="mt-1">
<input
@@ -339,7 +343,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
id="aroundlink"
placeholder="https://www.riverside.fm/studio/rick"
required
className={"block w-full rounded-sm border-gray-300 text-sm"}
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={
defaultValues
? defaultValues.find(
@@ -366,7 +370,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}></CheckboxField>
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
@@ -393,7 +398,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
)}
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"></div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left" />
</div>
{booking && (
<>
@@ -58,7 +58,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<ClockIcon className="m-auto h-6 w-6"></ClockIcon>
<ClockIcon className="m-auto h-6 w-6" />
</div>
<div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} />
@@ -175,7 +175,7 @@ export default function CreateEventTypeButton(props: Props) {
</Dropdown>
)}
<DialogContent>
<DialogContent className="overflow-y-auto">
<div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
@@ -200,16 +200,26 @@ export default function CreateEventTypeButton(props: Props) {
)}
<TextField label={t("title")} placeholder={t("quick_chat")} {...register("title")} />
<TextField
label={t("url")}
required
addOnLeading={
<InputLeading>
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
</InputLeading>
}
{...register("slug")}
/>
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
<TextField
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
required
addOnLeading={<InputLeading>/{pageSlug}/</InputLeading>}
{...register("slug")}
/>
) : (
<TextField
label={t("url")}
required
addOnLeading={
<InputLeading>
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
</InputLeading>
}
{...register("slug")}
/>
)}
<TextAreaField
label={t("description")}
@@ -247,7 +257,7 @@ export default function CreateEventTypeButton(props: Props) {
<RadioArea.Group
{...register("schedulingType")}
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
className="relative mt-1 flex space-x-6 rounded-sm shadow-sm rtl:space-x-reverse">
className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse">
<RadioArea.Item
value={SchedulingType.COLLECTIVE}
defaultChecked={type === SchedulingType.COLLECTIVE}
@@ -82,7 +82,7 @@ export default function RecurringEventController({
type="number"
min="1"
max="20"
className="block w-16 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
className="block w-16 rounded-sm border-gray-300 [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
defaultValue={recurringEventState.interval}
onChange={(event) => {
const newVal = {
@@ -114,7 +114,7 @@ export default function RecurringEventController({
type="number"
min="1"
max="20"
className="block w-16 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
className="block w-16 rounded-sm border-gray-300 [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
defaultValue={recurringEventState.count}
onChange={(event) => {
const newVal = {
@@ -8,10 +8,10 @@ function SkeletonLoader() {
return (
<SkeletonContainer>
<div className="mb-4 flex items-center">
<SkeletonAvatar width="8" height="8"></SkeletonAvatar>
<SkeletonAvatar width="8" height="8" />
<div className="space-y-1">
<SkeletonText height="4" width="16"></SkeletonText>
<SkeletonText height="4" width="24"></SkeletonText>
<SkeletonText height="4" width="16" />
<SkeletonText height="4" width="24" />
</div>
</div>
<ul className="divide-y divide-neutral-200 border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
@@ -30,17 +30,17 @@ function SkeletonItem() {
<li className="group flex w-full items-center justify-between px-4 py-4 sm:px-6">
<div className="flex-grow truncate text-sm">
<div>
<SkeletonText width="32" height="5"></SkeletonText>
<SkeletonText width="32" height="5" />
</div>
<div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap">
<ClockIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200"></ClockIcon>
<SkeletonText width="12" height="4"></SkeletonText>
<ClockIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText width="12" height="4" />
</li>
<li className="flex items-center whitespace-nowrap">
<UserIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200"></UserIcon>
<SkeletonText width="16" height="4"></SkeletonText>
<UserIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
<SkeletonText width="16" height="4" />
</li>
</ul>
</div>
@@ -74,7 +74,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
type="text"
id="label"
required
className="block w-full rounded-sm border-gray-300 text-sm shadow-sm"
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={selectedCustomInput?.label}
{...register("label", { required: true })}
/>
@@ -90,7 +90,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
<input
type="text"
id="placeholder"
className="block w-full rounded-sm border-gray-300 text-sm shadow-sm"
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={selectedCustomInput?.placeholder}
{...register("placeholder")}
/>
@@ -74,7 +74,7 @@ const ChangePasswordSection = () => {
name="current_password"
id="current_password"
required
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
className="block w-full rounded-sm border-gray-300 sm:text-sm"
placeholder={t("your_old_password")}
/>
</div>
@@ -91,7 +91,7 @@ const ChangePasswordSection = () => {
value={newPassword}
required
onInput={(e) => setNewPassword(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
className="block w-full rounded-sm border-gray-300 sm:text-sm"
placeholder={t("super_secure_new_password")}
/>
</div>
@@ -70,7 +70,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
required
value={password}
onInput={(e) => setPassword(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
className="block w-full rounded-sm border-gray-300 sm:text-sm"
/>
</div>
@@ -139,7 +139,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
required
value={password}
onInput={(e) => setPassword(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
className="block w-full rounded-sm border-gray-300 sm:text-sm"
/>
</div>
@@ -175,7 +175,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
minLength={6}
inputMode="numeric"
onInput={(e) => setTotpCode(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
className="block w-full rounded-sm border-gray-300 sm:text-sm"
autoComplete="one-time-code"
/>
</div>
@@ -91,7 +91,7 @@ export default function MemberChangeRoleModal(props: {
value={role}
onChange={(option) => option && setRole(option)}
id="role"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm sm:text-sm"
className="mt-1 block w-full rounded-md border-gray-300 sm:text-sm"
/>
</div>
{errorMessage && (
@@ -104,7 +104,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
options={props.currentMember !== MembershipRole.OWNER ? options.slice(0, 2) : options}
id="role"
name="role"
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
className="mt-1 block w-full rounded-sm border-gray-300 sm:text-sm"
/>
</div>
<div className="relative flex items-start">
@@ -114,7 +114,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
name="sendInviteEmail"
defaultChecked
id="sendInviteEmail"
className="rounded-sm border-gray-300 text-black shadow-sm sm:text-sm"
className="rounded-sm border-gray-300 text-black sm:text-sm"
/>
</div>
<div className="text-sm ltr:ml-2 rtl:mr-2">
+2 -2
View File
@@ -91,7 +91,7 @@ export default function MemberListItem(props: Props) {
<div className="mt-2 flex ltr:mr-2 rtl:ml-2 sm:mt-0 sm:justify-center">
{/* Tooltip doesn't show... WHY????? */}
{props.member.isMissingSeat && (
<Tooltip content={t("hidden_team_member_message")}>
<Tooltip side="top" content={t("hidden_team_member_message")}>
<TeamPill color="red" text={t("hidden")} />
</Tooltip>
)}
@@ -100,7 +100,7 @@ export default function MemberListItem(props: Props) {
</div>
</div>
<div className="flex">
<Tooltip content={t("team_view_user_availability")}>
<Tooltip side="top" content={t("team_view_user_availability")}>
<Button
// Disabled buttons don't trigger Tooltips
title={
@@ -23,18 +23,18 @@ function SkeletonItem() {
<li className="group flex w-full items-center justify-between px-3 py-4">
<div className="flex-grow truncate text-sm">
<div className="flex justify-start space-x-2 px-2">
<SkeletonText width="10" height="10" className="rounded-full"></SkeletonText>
<SkeletonText width="10" height="10" className="rounded-full" />
<div className="space-y-2">
<SkeletonText height="4" width="32"></SkeletonText>
<SkeletonText height="4" width="16"></SkeletonText>
<SkeletonText height="4" width="32" />
<SkeletonText height="4" width="16" />
</div>
</div>
</div>
<div className="mt-4 hidden flex-shrink-0 pr-4 sm:mt-0 sm:ml-5 lg:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
<SkeletonText width="12" height="4"></SkeletonText>
<SkeletonText width="4" height="4"></SkeletonText>
<SkeletonText width="4" height="4"></SkeletonText>
<SkeletonText width="12" height="4" />
<SkeletonText width="4" height="4" />
<SkeletonText width="4" height="4" />
</div>
</div>
</li>
+1 -1
View File
@@ -63,7 +63,7 @@ export default function TeamCreate(props: Props) {
id="name"
placeholder="Acme Inc."
required
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 sm:text-sm"
/>
</div>
{errorMessage && <Alert severity="error" title={errorMessage} />}
+40 -7
View File
@@ -5,6 +5,8 @@ import {
LinkIcon,
PencilIcon,
TrashIcon,
CheckIcon,
XIcon,
} from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client";
import Link from "next/link";
@@ -98,19 +100,50 @@ export default function TeamListItem(props: Props) {
<div className="px-5 py-5">
{isInvitee && (
<>
<Button type="button" color="secondary" onClick={declineInvite}>
{t("reject")}
</Button>
<Button type="button" color="primary" className="ltr:ml-2 rtl:mr-2" onClick={acceptInvite}>
{t("accept")}
</Button>
<div className="hidden sm:block">
<Button type="button" color="secondary" onClick={declineInvite}>
{t("reject")}
</Button>
<Button type="button" color="primary" className="ltr:ml-2 rtl:mr-2" onClick={acceptInvite}>
{t("accept")}
</Button>
</div>
<div className="block sm:hidden">
<Dropdown>
<DropdownMenuTrigger className="group h-10 w-10 border border-transparent p-0 text-neutral-400 hover:border-gray-200 ">
<DotsHorizontalIcon className="h-5 w-5 group-hover:text-gray-800" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Button
color="minimal"
size="sm"
className="w-full rounded-none font-medium"
StartIcon={CheckIcon}
onClick={acceptInvite}>
{t("accept")}
</Button>
</DropdownMenuItem>
<DropdownMenuItem>
<Button
color="warn"
size="sm"
className="w-full rounded-none font-medium"
StartIcon={XIcon}
onClick={declineInvite}>
{t("reject")}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
</>
)}
{!isInvitee && (
<div className="flex rtl:space-x-reverse">
<TeamRole role={team.role} />
<Tooltip content={t("copy_link_team")}>
<Tooltip side="top" content={t("copy_link_team")}>
<Button
onClick={() => {
navigator.clipboard.writeText(process.env.NEXT_PUBLIC_WEBSITE_URL + "/team/" + team.slug);
+6 -5
View File
@@ -92,7 +92,7 @@ export default function TeamSettings(props: Props) {
id="team-url"
addOnLeading={
<span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{"team/"}
{process.env.NEXT_PUBLIC_WEBSITE_URL}/team/
</span>
}
ref={teamUrlRef}
@@ -112,7 +112,7 @@ export default function TeamSettings(props: Props) {
id="name"
placeholder={t("your_team_name")}
required
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 sm:text-sm"
defaultValue={team?.name as string}
/>
}
@@ -131,7 +131,8 @@ export default function TeamSettings(props: Props) {
name="about"
rows={3}
defaultValue={team?.bio as string}
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"></textarea>
className="mt-1 block w-full rounded-sm border-gray-300 sm:text-sm"
/>
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
</>
}
@@ -140,7 +141,7 @@ export default function TeamSettings(props: Props) {
<div>
<SettingInputContainer
Icon={PhotographIcon}
label={"Logo"}
label="Logo"
htmlFor="avatar"
Input={
<>
@@ -151,7 +152,7 @@ export default function TeamSettings(props: Props) {
name="avatar"
id="avatar"
placeholder="URL"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 sm:text-sm"
defaultValue={team?.logo ?? undefined}
/>
<ImageUploader
@@ -120,7 +120,7 @@ export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers;
{props.team?.id && props.role !== MembershipRole.MEMBER && (
<Link href={`/settings/teams/${props.team.id}/availability`}>
<div className="mt-5 hidden space-y-1 sm:block">
<LinkIconButton Icon={ClockIcon}>{"View Availability"}</LinkIconButton>
<LinkIconButton Icon={ClockIcon}>View Availability</LinkIconButton>
<p className="mt-2 text-sm text-gray-500">See your team members availability at a glance.</p>
</div>
</Link>
@@ -50,7 +50,7 @@ export function UpgradeToFlexibleProModal(props: Props) {
setErrorMessage(null);
}}>
<DialogTrigger asChild>
<a className="cursor-pointer underline">{"Upgrade Now"}</a>
<a className="cursor-pointer underline">Upgrade Now</a>
</DialogTrigger>
<DialogContent>
<DialogHeader title={t("Purchase missing seats")} />
+3 -1
View File
@@ -1,6 +1,8 @@
import classNames from "classnames";
import React from "react";
import { LOGO } from "@calcom/lib/constants";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";
@@ -20,7 +22,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
{props.showLogo && (
// eslint-disable-next-line @next/next/no-img-element
<img className="mx-auto h-6" src="/calendso-logo-white-word.svg" alt="Cal.com Logo" />
<img className="mx-auto h-6" src={LOGO} alt="Cal.com Logo" />
)}
{props.heading && (
<h2 className="font-cal mt-6 text-center text-3xl text-neutral-900">{props.heading}</h2>
+4 -1
View File
@@ -15,6 +15,9 @@ export type AvatarProps = {
gravatarFallbackMd5?: string;
};
/**
* @deprecated Use AvatarSSR instead. Once, there is no usage of Avatar, AvatarSSR can be renamed.
*/
export default function Avatar(props: AvatarProps) {
const { imageSrc, gravatarFallbackMd5, size, alt, title } = props;
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
@@ -37,7 +40,7 @@ export default function Avatar(props: AvatarProps) {
return title ? (
<Tooltip.Tooltip delayDuration={300}>
<Tooltip.TooltipTrigger className="cursor-default">{avatar}</Tooltip.TooltipTrigger>
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white shadow-sm">
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white">
<Tooltip.Arrow />
{title}
</Tooltip.Content>
+2 -1
View File
@@ -3,6 +3,7 @@ import React from "react";
import classNames from "@lib/classNames";
import Avatar from "@components/ui/Avatar";
import { AvatarSSR } from "@components/ui/AvatarSSR";
export type AvatarGroupProps = {
border?: string; // this needs to be the color of the parent container background, i.e.: border-white dark:border-gray-900
@@ -23,7 +24,7 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
if (item.image != null) {
return (
<li key={idx} className="-mr-2 inline-block">
<Avatar
<AvatarSSR
className={props.border}
imageSrc={item.image}
title={item.title}
+42 -14
View File
@@ -1,9 +1,17 @@
import { User } from "@prisma/client";
import * as Tooltip from "@radix-ui/react-tooltip";
import classNames from "@lib/classNames";
export type AvatarProps = {
user: Pick<User, "name" | "username" | "avatar"> & { emailMd5?: string };
export type AvatarProps = (
| {
user: Pick<User, "name" | "username" | "avatar"> & { emailMd5?: string };
}
| {
user?: null;
imageSrc: string;
}
) & {
className?: string;
size?: number;
title?: string;
@@ -16,18 +24,38 @@ function defaultAvatarSrc(md5: string) {
}
// An SSR Supported version of Avatar component.
// FIXME: title support is missing
export function AvatarSSR(props: AvatarProps) {
const { user, size } = props;
const nameOrUsername = user.name || user.username || "";
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
let imgSrc;
const alt = props.alt || nameOrUsername;
if (user.avatar) {
imgSrc = user.avatar;
} else if (user.emailMd5) {
imgSrc = defaultAvatarSrc(user.emailMd5);
const { size, title } = props;
let imgSrc = "";
let alt: string = props.alt;
if (props.user) {
const user = props.user;
const nameOrUsername = user.name || user.username || "";
alt = alt || nameOrUsername;
if (user.avatar) {
imgSrc = user.avatar;
} else if (user.emailMd5) {
imgSrc = defaultAvatarSrc(user.emailMd5);
}
} else {
imgSrc = props.imageSrc;
}
// eslint-disable-next-line @next/next/no-img-element
return imgSrc ? <img alt={alt} className={className} src={imgSrc}></img> : null;
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
const avatar = imgSrc ? <img alt={alt} className={className} src={imgSrc} /> : null;
return title ? (
<Tooltip.Tooltip delayDuration={300}>
<Tooltip.TooltipTrigger className="cursor-default">{avatar}</Tooltip.TooltipTrigger>
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white">
<Tooltip.Arrow />
{title}
</Tooltip.Content>
</Tooltip.Tooltip>
) : (
<>{avatar}</>
);
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { Tooltip } from "@calcom/ui/Tooltip";
export default function InfoBadge({ content }: { content: string }) {
return (
<>
<Tooltip content={content}>
<Tooltip side="top" content={content}>
<span title={content}>
<InformationCircleIcon className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" />
</span>
+2 -1
View File
@@ -1,6 +1,7 @@
import Link from "next/link";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { POWERED_BY_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
const PoweredByCal = () => {
@@ -8,7 +9,7 @@ const PoweredByCal = () => {
const isEmbed = useIsEmbed();
return (
<div className={"p-1 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
<Link href={`https://cal.com?utm_source=embed&utm_medium=powered-by-button`}>
<Link href={POWERED_BY_URL}>
<a target="_blank" className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
{t("powered_by")}{" "}
{
@@ -0,0 +1,332 @@
import { CheckIcon, ExternalLinkIcon, PencilAltIcon, StarIcon, XIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import { debounce } from "lodash";
import { MutableRefObject, useCallback, useEffect, useState } from "react";
import { fetchUsername } from "@calcom/lib/fetchUsername";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog";
import { Input, Label } from "@calcom/ui/form/fields";
import { trpc } from "@lib/trpc";
import { AppRouter } from "@server/routers/_app";
import { TRPCClientErrorLike } from "@trpc/client";
export enum UsernameChangeStatusEnum {
NORMAL = "NORMAL",
UPGRADE = "UPGRADE",
DOWNGRADE = "DOWNGRADE",
}
interface ICustomUsernameProps {
currentUsername: string | undefined;
setCurrentUsername: (value: string | undefined) => void;
inputUsernameValue: string | undefined;
usernameRef: MutableRefObject<HTMLInputElement>;
setInputUsernameValue: (value: string) => void;
onSuccessMutation?: () => void;
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
user: Pick<
User,
| "username"
| "name"
| "email"
| "bio"
| "avatar"
| "timeZone"
| "weekStart"
| "hideBranding"
| "theme"
| "plan"
| "brandColor"
| "darkBrandColor"
| "metadata"
| "timeFormat"
| "allowDynamicBooking"
>;
}
const PremiumTextfield = (props: ICustomUsernameProps) => {
const { t } = useLocale();
const {
currentUsername,
setCurrentUsername,
inputUsernameValue,
setInputUsernameValue,
usernameRef,
onSuccessMutation,
onErrorMutation,
user,
} = props;
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
const [markAsError, setMarkAsError] = useState(false);
const [openDialogSaveUsername, setOpenDialogSaveUsername] = useState(false);
const [usernameChangeCondition, setUsernameChangeCondition] = useState<UsernameChangeStatusEnum | null>(
null
);
const userIsPremium =
user && user.metadata && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
const [premiumUsername, setPremiumUsername] = useState(false);
const debouncedApiCall = useCallback(
debounce(async (username) => {
const { data } = await fetchUsername(username);
setMarkAsError(!data.available);
setPremiumUsername(data.premium);
setUsernameIsAvailable(data.available);
}, 150),
[]
);
useEffect(() => {
if (currentUsername !== inputUsernameValue) {
debouncedApiCall(inputUsernameValue);
} else if (inputUsernameValue === "") {
setMarkAsError(false);
setPremiumUsername(false);
setUsernameIsAvailable(false);
} else {
setPremiumUsername(userIsPremium);
setUsernameIsAvailable(false);
}
}, [inputUsernameValue]);
useEffect(() => {
if (usernameIsAvailable || premiumUsername) {
const condition = obtainNewUsernameChangeCondition({
userIsPremium,
isNewUsernamePremium: premiumUsername,
});
setUsernameChangeCondition(condition);
}
}, [usernameIsAvailable, premiumUsername]);
const obtainNewUsernameChangeCondition = ({
userIsPremium,
isNewUsernamePremium,
}: {
userIsPremium: boolean;
isNewUsernamePremium: boolean;
}) => {
let resultCondition: UsernameChangeStatusEnum;
if (!userIsPremium && isNewUsernamePremium) {
resultCondition = UsernameChangeStatusEnum.UPGRADE;
} else if (userIsPremium && !isNewUsernamePremium) {
resultCondition = UsernameChangeStatusEnum.DOWNGRADE;
} else {
resultCondition = UsernameChangeStatusEnum.NORMAL;
}
return resultCondition;
};
const utils = trpc.useContext();
const updateUsername = trpc.useMutation("viewer.updateProfile", {
onSuccess: async () => {
onSuccessMutation && (await onSuccessMutation());
setCurrentUsername(inputUsernameValue);
setOpenDialogSaveUsername(false);
},
onError: (error) => {
onErrorMutation && onErrorMutation(error);
},
async onSettled() {
await utils.invalidateQueries(["viewer.public.i18n"]);
},
});
const ActionButtons = (props: { index: string }) => {
const { index } = props;
return (usernameIsAvailable || premiumUsername) && currentUsername !== inputUsernameValue ? (
<div className="flex flex-row">
<Button
type="button"
className="mx-2"
onClick={() => setOpenDialogSaveUsername(true)}
data-testid={`update-username-btn-${index}`}>
{t("update")}
</Button>
<Button
type="button"
color="minimal"
className="mx-2"
onClick={() => {
if (currentUsername) {
setInputUsernameValue(currentUsername);
usernameRef.current.value = currentUsername;
}
}}>
{t("cancel")}
</Button>
</div>
) : (
<></>
);
};
const saveUsername = () => {
if (usernameChangeCondition === UsernameChangeStatusEnum.NORMAL) {
updateUsername.mutate({
username: inputUsernameValue,
});
}
};
return (
<>
<div style={{ display: "flex", justifyItems: "center" }}>
<Label htmlFor="username">{t("username")}</Label>
</div>
<div className="mt-1 flex rounded-md">
<span
className={classNames(
"inline-flex items-center rounded-l-sm border border-gray-300 bg-gray-50 px-3 text-sm text-gray-500"
)}>
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
</span>
<div style={{ position: "relative", width: "100%" }}>
<Input
ref={usernameRef}
name="username"
autoComplete="none"
autoCapitalize="none"
autoCorrect="none"
className={classNames(
"mt-0 rounded-l-none",
markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
: ""
)}
defaultValue={currentUsername}
onChange={(event) => {
event.preventDefault();
setInputUsernameValue(event.target.value);
}}
data-testid="username-input"
/>
{currentUsername !== inputUsernameValue && (
<div
className="top-0"
style={{
position: "absolute",
right: 2,
display: "flex",
flexDirection: "row",
}}>
<span
className={classNames(
"mx-2 py-1",
premiumUsername ? "text-orange-500" : "",
usernameIsAvailable ? "" : ""
)}>
{premiumUsername ? <StarIcon className="mt-[4px] w-6" /> : <></>}
{!premiumUsername && usernameIsAvailable ? <CheckIcon className="mt-[4px] w-6" /> : <></>}
</span>
</div>
)}
</div>
<div className="xs:hidden">
<ActionButtons index="desktop" />
</div>
</div>
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
{usernameIsAvailable && (
<p className={classNames("mt-1 text-xs text-gray-900")}>
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE && (
<>{t("standard_to_premium_username_description")}</>
)}
</p>
)}
{(usernameIsAvailable || premiumUsername) && currentUsername !== inputUsernameValue && (
<div className="mt-2 flex justify-end md:hidden">
<ActionButtons index="mobile" />
</div>
)}
<Dialog open={openDialogSaveUsername}>
<DialogContent>
<DialogClose asChild>
<div className="fixed top-1 right-1 flex h-8 w-8 justify-center rounded-full hover:bg-gray-200">
<XIcon className="w-4" />
</div>
</DialogClose>
<div style={{ display: "flex", flexDirection: "row" }}>
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<PencilAltIcon className="m-auto h-6 w-6" />
</div>
<div className="mb-4 w-full px-4 pt-1">
<DialogHeader title="Confirm username change" />
{usernameChangeCondition && usernameChangeCondition !== UsernameChangeStatusEnum.NORMAL && (
<p className="-mt-4 mb-4 text-sm text-gray-800">
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE &&
t("change_username_standard_to_premium")}
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE &&
t("change_username_premium_to_standard")}
</p>
)}
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
<div className="flex-1 px-2">
<p className="text-gray-500">
{t("current")} {t("username")}
</p>
<p className="mt-1" data-testid="current-username">
{currentUsername}
</p>
</div>
<div className="ml-6 flex-1">
<p className="text-gray-500" data-testid="new-username">
{t("new")} {t("username")}
</p>
<p>{inputUsernameValue}</p>
</div>
</div>
</div>
</div>
<div className="mt-4 flex flex-row-reverse gap-x-2">
{/* redirect to checkout */}
{(usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE ||
usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE) && (
<Button
type="button"
loading={updateUsername.isLoading}
data-testid="go-to-billing"
href={`/api/integrations/stripepayment/subscription?intentUsername=${inputUsernameValue}`}>
<>
{t("go_to_stripe_billing")} <ExternalLinkIcon className="ml-1 h-4 w-4" />
</>
</Button>
)}
{/* Normal save */}
{usernameChangeCondition === UsernameChangeStatusEnum.NORMAL && (
<Button
type="button"
loading={updateUsername.isLoading}
data-testid="save-username"
onClick={() => {
saveUsername();
}}>
{t("save")}
</Button>
)}
<DialogClose asChild>
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</>
);
};
export { PremiumTextfield };
@@ -0,0 +1,216 @@
import { CheckIcon, PencilAltIcon, XIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import { debounce } from "lodash";
import { MutableRefObject, useCallback, useEffect, useState } from "react";
import { fetchUsername } from "@calcom/lib/fetchUsername";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog";
import { Input, Label } from "@calcom/ui/form/fields";
import { trpc } from "@lib/trpc";
import { AppRouter } from "@server/routers/_app";
import { TRPCClientErrorLike } from "@trpc/client";
interface ICustomUsernameProps {
currentUsername: string | undefined;
setCurrentUsername: (value: string | undefined) => void;
inputUsernameValue: string | undefined;
usernameRef: MutableRefObject<HTMLInputElement>;
setInputUsernameValue: (value: string) => void;
onSuccessMutation?: () => void;
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
}
const UsernameTextfield = (props: ICustomUsernameProps) => {
const { t } = useLocale();
const {
currentUsername,
setCurrentUsername,
inputUsernameValue,
setInputUsernameValue,
usernameRef,
onSuccessMutation,
onErrorMutation,
} = props;
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
const [markAsError, setMarkAsError] = useState(false);
const [openDialogSaveUsername, setOpenDialogSaveUsername] = useState(false);
const debouncedApiCall = useCallback(
debounce(async (username) => {
const { data } = await fetchUsername(username);
setMarkAsError(!data.available);
setUsernameIsAvailable(data.available);
}, 150),
[]
);
useEffect(() => {
if (currentUsername !== inputUsernameValue) {
debouncedApiCall(inputUsernameValue);
} else if (inputUsernameValue === "") {
setMarkAsError(false);
setUsernameIsAvailable(false);
} else {
setUsernameIsAvailable(false);
}
}, [inputUsernameValue]);
const utils = trpc.useContext();
const updateUsername = trpc.useMutation("viewer.updateProfile", {
onSuccess: async () => {
onSuccessMutation && (await onSuccessMutation());
setCurrentUsername(inputUsernameValue);
setOpenDialogSaveUsername(false);
},
onError: (error) => {
onErrorMutation && onErrorMutation(error);
},
async onSettled() {
await utils.invalidateQueries(["viewer.public.i18n"]);
},
});
const ActionButtons = (props: { index: string }) => {
const { index } = props;
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
<div className="flex flex-row">
<Button
type="button"
className="mx-2"
onClick={() => setOpenDialogSaveUsername(true)}
data-testid={`update-username-btn-${index}`}>
{t("update")}
</Button>
<Button
type="button"
color="minimal"
className="mx-2"
onClick={() => {
if (currentUsername) {
setInputUsernameValue(currentUsername);
usernameRef.current.value = currentUsername;
}
}}>
{t("cancel")}
</Button>
</div>
) : (
<></>
);
};
return (
<>
<div>
<Label htmlFor="username">{t("username")}</Label>
</div>
<div className="mt-1 flex rounded-md">
<span
className={classNames(
"inline-flex items-center rounded-l-sm border border-gray-300 bg-gray-50 px-3 text-sm text-gray-500"
)}>
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
</span>
<div className="relative w-full">
<Input
ref={usernameRef}
name="username"
autoComplete="none"
autoCapitalize="none"
autoCorrect="none"
className={classNames(
"mt-0 rounded-l-none",
markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
: ""
)}
defaultValue={currentUsername}
onChange={(event) => {
event.preventDefault();
setInputUsernameValue(event.target.value);
}}
data-testid="username-input"
/>
{currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-0 flex flex-row">
<span className={classNames("mx-2 py-1")}>
{usernameIsAvailable ? <CheckIcon className="mt-[4px] w-6" /> : <></>}
</span>
</div>
)}
</div>
<div className="xs:hidden">
<ActionButtons index="desktop" />
</div>
</div>
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
{usernameIsAvailable && currentUsername !== inputUsernameValue && (
<div className="mt-2 flex justify-end md:hidden">
<ActionButtons index="mobile" />
</div>
)}
<Dialog open={openDialogSaveUsername}>
<DialogContent>
<DialogClose asChild>
<div className="fixed top-1 right-1 flex h-8 w-8 justify-center rounded-full hover:bg-gray-200">
<XIcon className="w-4" />
</div>
</DialogClose>
<div style={{ display: "flex", flexDirection: "row" }}>
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<PencilAltIcon 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">
<div className="flex-1 px-2">
<p className="text-gray-500">
{t("current")} {t("username").toLocaleLowerCase()}
</p>
<p className="mt-1" data-testid="current-username">
{currentUsername}
</p>
</div>
<div className="flex-1">
<p className="text-gray-500" data-testid="new-username">
{t("new")} {t("username").toLocaleLowerCase()}
</p>
<p>{inputUsernameValue}</p>
</div>
</div>
</div>
</div>
<div className="mt-4 flex flex-row-reverse gap-x-2">
<Button
type="button"
loading={updateUsername.isLoading}
data-testid="save-username"
onClick={() => {
updateUsername.mutate({
username: inputUsernameValue,
});
}}>
{t("save")}
</Button>
<DialogClose asChild>
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</>
);
};
export { UsernameTextfield };
@@ -0,0 +1,6 @@
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { PremiumTextfield } from "./PremiumTextfield";
import { UsernameTextfield } from "./UsernameTextfield";
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
-64
View File
@@ -1,64 +0,0 @@
import React, { useEffect, useState } from "react";
interface WeekdaySelectProps {
defaultValue: number[];
onSelect: (selected: number[]) => void;
}
export const WeekdaySelect = (props: WeekdaySelectProps) => {
const { onSelect } = props;
const [activeDays, setActiveDays] = useState<boolean[]>(
Array.from(Array(7).keys()).map((v, i) => (props.defaultValue || []).includes(i))
);
const days = ["S", "M", "T", "W", "T", "F", "S"];
useEffect(() => {
onSelect(activeDays.map((v, idx) => (v ? idx : -1)).filter((v) => v !== -1));
}, [onSelect, activeDays]);
const toggleDay = (idx: number) => {
activeDays[idx] = !activeDays[idx];
setActiveDays(([] as boolean[]).concat(activeDays));
};
return (
<div className="weekdaySelect">
<div className="inline-flex">
{days.map((day, idx) =>
activeDays[idx] ? (
<button
key={idx}
onClick={(e) => {
e.preventDefault();
toggleDay(idx);
}}
className={`
bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast
h-10 w-10 rounded px-3 py-1 focus:outline-none
${activeDays[idx + 1] ? "rounded-r-none" : ""}
${activeDays[idx - 1] ? "rounded-l-none" : ""}
${idx === 0 ? "rounded-l" : ""}
${idx === days.length - 1 ? "rounded-r" : ""}
`}>
{day}
</button>
) : (
<button
key={idx}
onClick={(e) => {
e.preventDefault();
toggleDay(idx);
}}
style={{ marginTop: "1px", marginBottom: "1px" }}
className={`h-10 w-10 rounded-none bg-gray-50 px-3 py-1 focus:outline-none ${
idx === 0 ? "rounded-l" : "border-l-0"
} ${idx === days.length - 1 ? "rounded-r" : ""}`}>
{day}
</button>
)
)}
</div>
</div>
);
};
-8
View File
@@ -1,8 +0,0 @@
import { Alert } from "@calcom/ui/Alert";
/**
* @deprecated use `<Alert severity="error" message="x" />` instead
*/
export default function ErrorAlert(props: { message: string; className?: string }) {
return <Alert severity="error" message={props.message} />;
}
+1 -1
View File
@@ -87,7 +87,7 @@ const ColorPicker = (props: ColorPickerProps) => {
</div>
)}
<HexColorInput
className="ml-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
className="ml-1 block w-full rounded-sm border border-gray-300 px-3 py-2 sm:text-sm"
color={color}
onChange={(val) => {
setColor(val);
@@ -54,7 +54,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
<span className="text-sm ltr:ml-3 rtl:mr-3">{description}</span>
</>
)}
{informationIconText && <InfoBadge content={informationIconText}></InfoBadge>}
{informationIconText && <InfoBadge content={informationIconText} />}
</div>
</div>
</div>
+1 -1
View File
@@ -18,7 +18,7 @@ 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 shadow-sm sm:text-sm",
"focus:ring-primary-500 focus:border-primary-500 rounded-sm border border-gray-300 p-1 pl-2 sm:text-sm",
className
)}
clearIcon={null}
+1 -1
View File
@@ -16,7 +16,7 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
</div>
)}
<div className="w-full">
<div className="relative rounded-sm shadow-sm">
<div className="relative rounded-sm">
<input
{...rest}
ref={ref}
@@ -0,0 +1,94 @@
import React, { Dispatch, SetStateAction } from "react";
import { components, GroupBase, OptionProps } from "react-select";
import { Props } from "react-select";
import Select from "@calcom/ui/form/Select";
import { useLocale } from "@lib/hooks/useLocale";
export type Option = {
value: string;
label: string;
};
const InputOption = ({
isDisabled,
isFocused,
isSelected,
children,
innerProps,
...rest
}: OptionProps<any, boolean, GroupBase<any>>) => {
const style = {
alignItems: "center",
backgroundColor: isFocused ? "rgba(244, 245, 246, var(--tw-bg-opacity))" : "transparent",
color: "inherit",
display: "flex ",
};
const props = {
...innerProps,
style,
};
return (
<components.Option
{...rest}
isDisabled={isDisabled}
isFocused={isFocused}
isSelected={isSelected}
innerProps={props}>
<input
type="checkbox"
className="text-primary-600 focus:ring-primary-500 mr-2 h-4 w-4 rounded border-gray-300"
checked={isSelected}
readOnly
/>
{children}
</components.Option>
);
};
type MultiSelectionCheckboxesProps = {
options: { label: string; value: string }[];
setSelected: Dispatch<SetStateAction<Option[]>>;
selected: Option[];
setValue: (s: Option[]) => unknown;
};
const MultiValue = ({ index, getValue }: { index: number; getValue: any }) => {
const { t } = useLocale();
return <>{!index && <div>{t("nr_event_type", { count: getValue().length })}</div>}</>;
};
export default function MultiSelectCheckboxes({
options,
isLoading,
selected,
setSelected,
setValue,
}: Omit<Props, "options"> & MultiSelectionCheckboxesProps) {
const additonalComponents = { MultiValue };
return (
<Select
value={selected}
onChange={(s: any) => {
setSelected(s);
setValue(s);
}}
options={options}
isMulti
className="w-64 text-sm"
isSearchable={false}
closeMenuOnSelect={false}
hideSelectedOptions={false}
isLoading={isLoading}
components={{
...additonalComponents,
Option: InputOption,
}}
/>
);
}
+3 -6
View File
@@ -1,8 +1,6 @@
import BasePhoneInput, { Props } from "react-phone-number-input/react-hook-form";
import "react-phone-number-input/style.css";
import classNames from "@lib/classNames";
export type PhoneInputProps<FormValues> = Props<
{
value: string;
@@ -19,10 +17,9 @@ function PhoneInput<FormValues>({ control, name, ...rest }: PhoneInputProps<Form
{...rest}
name={name}
control={control}
className={classNames(
"border-1 focus-within:border-brand block w-full rounded-sm border border-gray-300 py-px px-3 shadow-sm ring-black focus-within:ring-1 dark:border-black dark:bg-black dark:text-white",
rest.disabled ? "bg-gray-200 dark:text-gray-500" : ""
)}
countrySelectProps={{ className: "text-black" }}
numberInputProps={{ className: "border-0 text-sm focus:ring-0 dark:bg-gray-700" }}
className="border-1 focus-within:border-brand block w-full rounded-sm border border-gray-300 py-px pl-3 ring-black focus-within:ring-1 disabled:text-gray-500 disabled:opacity-50 dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500"
/>
);
}
+70 -3
View File
@@ -1,5 +1,5 @@
import React from "react";
import ReactSelect, { components, GroupBase, Props, InputProps } from "react-select";
import React, { useCallback, useEffect, useState } from "react";
import ReactSelect, { components, GroupBase, Props, InputProps, SingleValue, MultiValue } from "react-select";
import classNames from "@lib/classNames";
@@ -55,10 +55,77 @@ function Select<
IndicatorSeparator: () => null,
Input: InputComponent,
}}
className={classNames("text-sm shadow-sm", className)}
className={classNames("text-sm", className)}
{...props}
/>
);
}
export function SelectWithValidation<
Option extends { label: string; value: string },
isMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>
>({
required = false,
onChange,
value,
...remainingProps
}: SelectProps<Option, isMulti, Group> & { required?: boolean }) {
const [hiddenInputValue, _setHiddenInputValue] = useState(() => {
if (value instanceof Array || !value) {
return;
}
return value.value || "";
});
const setHiddenInputValue = useCallback((value: MultiValue<Option> | SingleValue<Option>) => {
let hiddenInputValue = "";
if (value instanceof Array) {
hiddenInputValue = value.map((val) => val.value).join(",");
} else {
hiddenInputValue = value?.value || "";
}
_setHiddenInputValue(hiddenInputValue);
}, []);
useEffect(() => {
if (!value) {
return;
}
setHiddenInputValue(value);
}, [value, setHiddenInputValue]);
return (
<div className={classNames("relative", remainingProps.className)}>
<Select
value={value}
{...remainingProps}
onChange={(value, ...remainingArgs) => {
setHiddenInputValue(value);
if (onChange) {
onChange(value, ...remainingArgs);
}
}}
/>
{required && (
<input
tabIndex={-1}
autoComplete="off"
style={{
opacity: 0,
width: "100%",
height: 1,
position: "absolute",
}}
value={hiddenInputValue}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange={() => {}}
// TODO:Not able to get focus to work
// onFocus={() => selectRef.current?.focus()}
required={required}
/>
)}
</div>
);
}
export default Select;
@@ -42,7 +42,7 @@ function TimezoneSelect({ className, ...props }: SelectProps) {
IndicatorSeparator: () => null,
Input: InputComponent,
}}
className={classNames("text-sm shadow-sm", className)}
className={classNames("text-sm", className)}
{...props}
/>
);
@@ -34,7 +34,7 @@ export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
type="button"
disabled={disabled}
className={classNames(
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left shadow-sm sm:text-sm",
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left sm:text-sm",
disabled && "cursor-default bg-gray-200 focus:ring-0 "
)}>
{getLabel(props.value) ?? placeholder}
@@ -51,16 +51,17 @@ export default function WebhookListItem(props: { webhook: TWebhook; onEditWebhoo
</div>
</div>
<div className="flex">
<Tooltip content={t("edit_webhook")}>
<Tooltip side="top" content={t("edit_webhook")}>
<Button
onClick={() => props.onEditWebhook()}
color="minimal"
size="icon"
StartIcon={PencilAltIcon}
className="ml-4 w-full self-center p-2"></Button>
className="ml-4 w-full self-center p-2"
/>
</Tooltip>
<Dialog>
<Tooltip content={t("delete_webhook")}>
<Tooltip side="top" content={t("delete_webhook")}>
<DialogTrigger asChild>
<Button
onClick={(e) => {
@@ -69,7 +70,8 @@ export default function WebhookListItem(props: { webhook: TWebhook; onEditWebhoo
color="minimal"
size="icon"
StartIcon={TrashIcon}
className="ml-2 w-full self-center p-2"></Button>
className="ml-2 w-full self-center p-2"
/>
</DialogTrigger>
</Tooltip>
<ConfirmationDialogContent
@@ -24,7 +24,7 @@ export default function WebhookTestDisclosure() {
return (
<Collapsible open={open} onOpenChange={() => setOpen(!open)}>
<CollapsibleTrigger type="button" className={"flex w-full cursor-pointer"}>
<CollapsibleTrigger type="button" className="flex w-full cursor-pointer">
<ChevronRightIcon className={`${open ? "rotate-90 transform" : ""} h-5 w-5 text-neutral-500`} />
<span className="text-sm font-medium text-gray-700">{t("webhook_test")}</span>
</CollapsibleTrigger>
+1 -1
View File
@@ -46,7 +46,7 @@ export default function LicenseBanner() {
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
<Dialog>
<DialogTrigger asChild>
<button className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 shadow-sm hover:bg-green-50">
<button className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
{t("accept_license")}
</button>
</DialogTrigger>
+3 -2
View File
@@ -32,8 +32,9 @@ const LicenseRequired: FC<LicenseRequiredProps> = ({ children, as = "", ...rest
To enable this feature, get a deployment key at{" "}
<a href={CONSOLE_URL} target="_blank" rel="noopener noreferrer" className="underline">
Cal.com console
</a>
. If your team already has a license, please contact{" "}
</a>{" "}
and add it to your .env as <code>CALCOM_LICENSE_KEY</code>. If your team already has a license,
please contact{" "}
<a href="mailto:peer@cal.com" className="underline">
peer@cal.com
</a>{" "}
+5 -3
View File
@@ -1,5 +1,4 @@
import dayjs from "dayjs";
import dayjs from "@calcom/dayjs";
import Button from "@calcom/ui/Button";
import { TRIAL_LIMIT_DAYS } from "@lib/config/constants";
@@ -23,7 +22,10 @@ const TrialBanner = () => {
<div
className="m-4 hidden rounded-md bg-yellow-200 p-4 text-center text-sm font-medium text-gray-600 lg:block"
data-testid="trial-banner">
<div className="mb-2 text-left">{t("trial_days_left", { days: trialDaysLeft })}</div>
<div className="mb-2 text-left">
{trialDaysLeft > 0 ? t("trial_days_left", { days: trialDaysLeft }) : t("trial_expired")}
</div>
<Button
href="/api/upgrade"
color="minimal"

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