401 Commits

Author SHA1 Message Date
zomars
4099a477d1 v1.5.4 2022-05-09 14:33:32 -06:00
zomars
6542da7e30 Formatting 2022-05-09 14:33:13 -06:00
Hariom Balhara
8336611f54 Missing translation (#2697)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-09 22:24:01 +02:00
zomars
819c6c96e8 Build fixes 2022-05-09 14:05:57 -06:00
zomars
e9ff358ac2 Update BookingPage.tsx 2022-05-09 13:59:45 -06:00
zomars
f79fd36c03 Merge branch 'production' 2022-05-09 13:58:18 -06:00
Alex van Andel
edd99cdeb2 Replaces member avatars with links to avatar.png endpoint (#2708)
* Replaces member avatars with links to avatar.png endpoint

* Replaced additional occurences

* Use WEBSITE_URL from @calcom/lib/constants instead

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-09 18:25:12 +00:00
Hariom Balhara
9825754b32 Hotfix: blank page for booking embed in Incognito Chrome (#2700)
* Merge remote-tracking branch 'origin/main' into feat/success-url

* Fix localstorage access

* Fix Comments

* make custom eleemnt explicitly 100% in width to go full width in a flex type parent

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-09 18:12:47 +00:00
Omar López
6a18b40c97 Update package.json
Fixes v14 builds
2022-05-09 12:00:49 -06:00
Hariom Balhara
d00f0bae1d All non recurring bookings were clubbed into one distinct booking (#2706) 2022-05-09 16:04:42 +00:00
sean-brydon
fb042a36b6 Fix Mobile UI for /settings/security (#2703)
* Fix Mobile UI

* Remove truncate

* Move buttons

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-05-09 13:43:27 +02:00
Peer Richelsen
6c27b04f83 fixed team dark mode (#2702) 2022-05-09 11:25:36 +00:00
Syed Ali Shahbaz
9322b4ab4c Flow, UX and other improvements for hash my url feature (#2644)
* added toast feedback

* updated flow

* locale

* updated locale data

* removed unused booking call for reschedule flow

* fixed hashedURL test

* test adjustment

* further test changes

* added check in test to click check only if unchecked

* Added private link quick copy button

* fixed spacing

* fix lint

* consistency

* moved create hash function out of component render

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-09 16:41:07 +05:30
Peer Richelsen
5464d4c010 Update README.md 2022-05-09 12:33:39 +02:00
sean-brydon
351622c4a2 Add Invalid Email Error (#2637)
* Add error message UI

* Add border color

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-05-09 08:24:39 +00:00
Hariom Balhara
44736ac461 Fix turbo caching (#2695)
* Fix turbo cachin

* Improve tests stability
2022-05-07 08:54:30 +00:00
Hariom Balhara
a2da95b12b Fix app (#2694) 2022-05-07 05:56:29 +00:00
zomars
8ae5b68504 Adds hint to input 2022-05-06 16:09:40 -06:00
zomars
1a3c3af072 Tooltip export fixes 2022-05-06 15:59:15 -06:00
zomars
5dde542952 Form fixes 2022-05-06 15:46:31 -06:00
zomars
4922a13b68 Form warning fixes 2022-05-06 15:44:57 -06:00
zomars
a05860515e Form fixes 2022-05-06 15:44:51 -06:00
zomars
7399d6421e Form warning fixes 2022-05-06 15:30:46 -06:00
iamkun
269dea70a1 fix: Booking page display time based on selected timezone (#2691)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-06 19:28:09 +00:00
alannnc
46690fa72b fix/scroll-mobile-on-dialog (#2693)
* Added mobile first css props for scroll

* Fix submodule previous add

* Fix mobile first css for dialog

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-06 18:34:48 +00:00
alannnc
8c9096b55b Vital App - Auto reschedule based on health data (#2500)
* Add vital integration

* Tidy up client_user_id creation

* Rename vital app to vitalother to follow name rules

* Added env var

* App vital reschedule

* Fix on app structure and api calls

* Implemented user identification from webhook

* WIP fix api call and read me

* Save vital settings via api

* Now saving userVitalSettings and trigger reschedule on selected param

* Added translations

* Fix type for vitalSettings

* Using api to get env vars required for url, fix display of vital settings

* Fix hours placeholder, translation not working

* Renames vital app

* Update seed-app-store.ts

* Update package.json

* Update yarn.lock

* Refactored env variables

* Update README.md

* Migrates to api_keys

* Extracts AppConfiguration

* vitalClient fixes

* Update index.ts

* Update metadata.ts

* Update index.ts

* Update metadata.ts

* Added namespace vital for translations

Co-authored-by: Maitham <maithamdib@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-06 17:21:30 +00:00
Hariom Balhara
67cc3a6409 Embed Code Generator: Fix Preview HTML and Embed Lib path for production (#2688)
* Improve logging

* Improve logging

* Keep embed origin conigurable

* Make embed URL and embed origin conigurable through env

* Gitignore public embed

* Add fingerprint to preview as well

* Fix path

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-06 15:56:26 +00:00
Carina Wollendorfer
83ec6d69eb fix query to list API keys (#2690)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-06 14:48:27 +00:00
Joe Au-Yeung
65a76b96c6 Add missing info to success page (#2680)
* Add missing info to success page

* Remove console.logs

* Add localized format to time
2022-05-06 08:15:05 -06:00
Hariom Balhara
dd7f22e021 Fix border in dark theme (#2687) 2022-05-06 12:09:12 +02:00
zomars
1a06d9906b Fixes daily-video slug 2022-05-05 16:36:30 -06:00
zomars
6fb301970b Fixes daily-video slug 2022-05-05 16:34:26 -06:00
zomars
3baf7060f7 Adds console url to redirection whitelist 2022-05-05 16:29:38 -06:00
zomars
0b82b85166 Adds console url to redirection whitelist 2022-05-05 16:29:17 -06:00
zomars
82dfd807c8 All apps in DB are installed by default 2022-05-05 16:21:52 -06:00
zomars
af9612968b Fixes missing slack credentials 2022-05-05 16:21:52 -06:00
zomars
70455f56a2 All apps in DB are installed by default 2022-05-05 16:18:28 -06:00
zomars
f839fd2bb4 Fixes missing slack credentials 2022-05-05 16:15:40 -06:00
Leo Giovanetti
1a79e0624c Recurring Events (#2562)
* Init dev

* UI changes for recurring event + prisma

* Revisiting schema + changes WIP

* UI done, BE WIP

* Feature completion

* Unused query param removed

* Invalid comment removed

* Removed unused translation

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

Thanks!

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

* Success page changes

* More progress

* Email text tweaks + test + seed

* Tweaking emails + Cal Apps support WIP

* No app integration for now
Final email and pages tweaks to avoid recurring info showed

* Missing comment for clarity

* Yet again, comment

* Last minute fix

* Missing tooltip for upcoming bookings

* Fixing seed

* Fixing import

* Increasing timeout for e2e

* Fixing any

* Apply suggestions from code review

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

* Update apps/web/pages/d/[link]/book.tsx

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

* Code improvements

* More code improvements

* Reverting back number input arrows

* Update BookingPage.tsx

* Update BookingPage.tsx

* Adds fallback for sendOrganizerPaymentRefundFailedEmail

* Type overkill

* Type fixes

* Type fixes

* Nitpicks

* Update success.tsx

* Update success.tsx

* Update success.tsx

* Fixing types

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2022-05-05 18:16:25 -03:00
iamkun
26e46ff06c fix: DatePicker to display the correct available hours (#2686) 2022-05-05 18:51:22 +00:00
Agusti Fernandez Pardo
71e67b50b2 Fix: Improve docs and mobile style of api docs (#2635)
* fix: adds servers in openapi, remove hack in snippets, update deps, make dynamic import to use latests swagger ui deps

* fix: remove unneded import

* fix: adds yarn dev commands for api and swagger

* fix: prisma not web before api

* fix: improve mobile docs api

* fix request snippets

* fix: custom more visible scrollbar for snippets in moible

* fix: remove comments and ugly scrollbar

* fix: types and remove lib url

* fix: install iframe-react-resizer in docs

* fix: remove web scope from yarn dev:api

* fix: remove json-schema autogenerated as wont be used

* fix: apiKeyAuth

* fix: swagger patch thx hariom

* fix: add api to env/example

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-05-05 09:36:02 -06:00
Hariom Balhara
174ed9f6d1 Embed Snippet Generator (#2597)
* Add support to dynamically change the theme

* Add Embed UI in app

* Update UI as per Figma

* Dynamicaly update Embed Code

* Get differnet modes working in preview

* Support Embed on EventType Edit, Team Link Fix and Mobile unsupported

* Fix auto theme switch in Embed Snippet generator

* Fix types

* Self Review fixes

* Remove Embed from App section

* Move get query after the middleware to let middleware work on it

* Add sandboxes in the document

* Add error handling for embed loading

* Fix types

* Update snapshots and fix bug identified by tests

* UI Fixes

* Add Embed Tests

* Respond in preview to width and height

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-05 08:29:49 -06:00
Carina Wollendorfer
60146ed2c5 fix redirect to setup page (#2683)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-05-05 09:32:08 +00:00
alannnc
df7abdfc06 Fix/reschedule on dynamic events (#2657)
* Reschedule for dynamic events

* Fix lint

* Handling attendee calendar event cancellation

Co-authored-by: zomars <zomars@me.com>
2022-05-05 01:03:36 +00:00
zomars
6ec2b20a23 Separates appStore env file 2022-05-04 16:13:28 -06:00
zomars
09d0f68c4c Update package.json 2022-05-04 16:09:58 -06:00
zomars
d6b7311c66 Build fixes 2022-05-04 15:28:58 -06:00
zomars
f1a2239c97 Linting and legibility 2022-05-04 15:28:58 -06:00
zomars
977ad141ee Extracts useMeQuery to own hook 2022-05-04 15:28:58 -06:00
Julián Sánchez
06f88eb5a3 Move method that gets the current user to a separate file 2022-05-04 15:28:58 -06:00
Julián Sánchez
daf39a4095 Fix problem related to data types 2022-05-04 15:28:58 -06:00
Julián David Sánchez Gallego
0973d79c31 Update way to get the 'accepted' attribute 2022-05-04 15:28:58 -06:00
Julián David Sánchez Gallego
257481bad5 Update restriction to change the role of other Owners 2022-05-04 15:28:58 -06:00
Julián David Sánchez Gallego
68e08f13a1 Change plain strings 2022-05-04 15:28:58 -06:00
Julián David Sánchez Gallego
3234898892 Add restrictions to protect the owners and change their roles 2022-05-04 15:28:58 -06:00
Joe Au-Yeung
82f7779a23 MS Teams title to state that a work/school account is required. (#2677)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-04 14:45:30 -04:00
zomars
bd9b83540b Jitsi slug fixes 2022-05-04 12:25:07 -06:00
zomars
4f0ee7b0b6 slug fixes 2022-05-04 12:25:07 -06:00
zomars
4437bfa840 Fix for undefined keys 2022-05-04 08:13:52 -06:00
zomars
b9e34c99ca Jitsi slug fixes 2022-05-03 22:44:24 -06:00
zomars
71fa872d5c slug fixes 2022-05-03 22:38:48 -06:00
zomars
377857915f Space booking test 2022-05-03 22:36:07 -06:00
zomars
ec5020ca3d Update index.ts 2022-05-03 22:30:46 -06:00
alannnc
f1bed08c13 feature/space-booking-app (#2673)
* Reschedule for dynamic events

* Fix lint

* feature/space-booking-app initial commit

* added loom video and fixes for main branch

* Revert previous commmit

* Renames spacebookingother to spacebooking

* Type and perf improvements

* Updated comment

* Update seed-app-store.ts

Co-authored-by: zomars <zomars@me.com>
2022-05-03 22:07:17 -06:00
alannnc
2cb663cd6a feature/space-booking-app (#2673)
* Reschedule for dynamic events

* Fix lint

* feature/space-booking-app initial commit

* added loom video and fixes for main branch

* Revert previous commmit

* Renames spacebookingother to spacebooking

* Type and perf improvements

* Updated comment

* Update seed-app-store.ts

Co-authored-by: zomars <zomars@me.com>
2022-05-04 04:06:20 +00:00
sean-brydon
fa1ca5fba0 Slack Signature Verification (#2667)
* Slack Verify

* Adding await

* Update slackVerify.ts

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-05-03 23:40:01 +00:00
Carina Wollendorfer
02b935bcde Feat/zapier app (#2623)
* create basic app structure

* add zapierSubscription model to prisma.schema

* change column name triggerEvent to lower case

* add zapier functionality + enpoints + adjust prisma.schema

* add subscriptionType + refactor code

* add app store information

* create setup page to generate api key

* clean code

* add copy functionality in setup page

* clean code

* add apiKeyType and delte key when uninstalled or new key generated

* clean code

* use Promise.all

* only approve zapier api key

* clean code

* fix findValidApiKey for api keys that don't expire

* fix migrations

* clean code

* small fixes

* add i18n

* add README.md file

* add setup guide to README.md

* fix yarn.lock

* Renames zapierother to zapier

* Typo

* Updates package name

* Rename fixes

* Adds zapier to the App Store seeder

* Adds missing zapier to apiHandlers

* Adds credential relationship to App

* Rename fixes

* Allows tailwind to pick up custom app-store components

* Consolidates zapier_setup_instructions

* Webhook fixes

* Uses app relationship instead of custom type

* Refactors sendPayload to accept webhook object

Instead of individual parameters

* refactoring

* Removes unused zapier check

* Update cancel.ts

* Refactoring

* Removes example comments

* Update InstallAppButton.tsx

* Type fixes

* E2E fixes

* Deletes all user zapier webhooks on integration removal

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-05-03 23:16:59 +00:00
zomars
b8d5c53813 Merge branch 'main' into production 2022-05-02 19:08:05 -06:00
zomars
bb90fe0d4b Marks installed by default 2022-05-02 19:05:49 -06:00
zomars
ba283e3dc0 Marks installed by default 2022-05-02 19:05:13 -06:00
zomars
9ae8a48dcd Type fixes 2022-05-02 18:21:29 -06:00
zomars
774f707c9f Type fixes 2022-05-02 18:21:09 -06:00
zomars
a6417c5757 Moar fixes 2022-05-02 18:00:36 -06:00
zomars
aebb610403 Moar fixes 2022-05-02 18:00:20 -06:00
zomars
fdbfd759af App env fixes 2022-05-02 17:59:06 -06:00
zomars
0213f66eb6 App env fixes 2022-05-02 17:59:06 -06:00
zomars
1de385a410 App env fixes 2022-05-02 17:42:25 -06:00
zomars
6011b440a8 App env fixes 2022-05-02 17:31:07 -06:00
zomars
de4b3c186e Hubspot fixes 2022-05-02 16:55:34 -06:00
zomars
4c8ff47ae7 Hubspot fixes 2022-05-02 16:50:52 -06:00
zomars
54269ba0bf v1.5.3 2022-05-02 16:21:11 -06:00
zomars
361579ba31 Migration seeder fixes 2022-05-02 16:21:11 -06:00
Joe Au-Yeung
000785c29f Add Meta Mask to app store (#2650)
* Adds available apps

* Adds App Model

* WIP

* Create meta mask app folder

* Add description and images

* Remove credential from installed apps page

* Updates seeder script

* Seeder fixes

* lowercase categories

* Upgrades prisma

* WIP

* WIP

* Hopefully fixes circular deps

* Type fixes

* Fixes seeder

* Adds migration to connect Credentials to Apps

* Updates app store callbacks

* Updates google credentials

* Uses dirName from DB

* Type fixes

* Update reschedule.ts

* Seeder fixes

* Fixes categories listing

* Update index.ts

* Update schema.prisma

* Updates dependencies

* Renames giphy app

* Uses dynamic imports for app metadata

* Fixes credentials error

* Uses dynamic import for api handlers

* Dynamic import fixes

* Allows for simple folder names in app store

* Remove video adaptor

* Squashes app migrations

* seeder fixes

* Renames to metamask

* Updates metamask metadata

* Fixes dyamic imports

* Remove comments

* Create migration.sql

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-05-02 16:21:11 -06:00
Omar López
2e6bc5e5b4 Fixes/app store keys in db (#2651)
* Adds available apps

* Adds App Model

* WIP

* Updates seeder script

* Seeder fixes

* lowercase categories

* Upgrades prisma

* WIP

* WIP

* Hopefully fixes circular deps

* Type fixes

* Fixes seeder

* Adds migration to connect Credentials to Apps

* Updates app store callbacks

* Updates google credentials

* Uses dirName from DB

* Type fixes

* Update reschedule.ts

* Seeder fixes

* Fixes categories listing

* Update index.ts

* Update schema.prisma

* Updates dependencies

* Renames giphy app

* Uses dynamic imports for app metadata

* Fixes credentials error

* Uses dynamic import for api handlers

* Dynamic import fixes

* Allows for simple folder names in app store

* Squashes app migrations

* seeder fixes

* Fixes dyamic imports

* Update apiHandlers.tsx
2022-05-02 16:21:11 -06:00
Shrey Gupta
11f6972ec9 feat(app-store): Add Giphy app (#2580) 2022-05-02 16:21:11 -06:00
Peer Richelsen
df801b4205 Update README.md 2022-05-02 16:21:11 -06:00
Peer Richelsen
433f2bafd8 Update index.mdx 2022-05-02 16:21:11 -06:00
Peer Richelsen
c5fc1a4648 Update PULL_REQUEST_TEMPLATE.md 2022-05-02 16:21:11 -06:00
sean-brydon
33d486b160 Enable Autocomplete (#2645)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-02 16:21:11 -06:00
Peer Richelsen
7e57c192ee added animations for dialog and tooltip (#2648)
* added animations for dialog and tooltip

* Update .env.example
2022-05-02 16:21:11 -06:00
Joe Au-Yeung
e4f7e26ad5 Hotfix - change calendar error message (#2643)
* Change calendar error message

* Change calendar error message

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-02 16:21:11 -06:00
Syed Ali Shahbaz
89b4acdfaf Hash my url (#2484)
* disposable link model added

* disposable model updated

* added disposable slug availability page

* added disposable book page

* added disposable slug hook

* added disposable link booking flow

* updated schema

* checktype fix

* added checkfix and schema generated

* create link API added

* added one time link view on event type list

* adjusted schema

* fixed disposable visual indicator

* expired check and visual indicator added

* updated slug for disposable event type

* revised schema

* WIP

* revert desc

* revert --WIP

* rework based on change of plans

* further adjustments

* added eventtype option for hashed link

* added refresh and delete on update

* fixed update call conditions

* cleanup

* code improvement

* clean up

* Potential fix for 404

* backward compat for booking page

* fixes regular booking for user and team

* typefix

* updated path for Booking import

* checkfix

* e2e wip

* link err fix

* workaround for banner issue in event type update-test

* added regenerate hash check

* fixed test according to new testID

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-05-02 16:21:11 -06:00
alannnc
d856ef53a7 Fix emails and cal event descriptions (#2634)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-02 16:21:11 -06:00
sean-brydon
3256d601db Fixes google meet url not appearing calendar invite (#2636)
* Update cal description

* Tidy up console logs

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-02 16:21:11 -06:00
Afzal Sayed
938f4f2b4d Pass userId while creating event-type (#2599)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-02 16:21:11 -06:00
sean-brydon
6999ab09b6 Fixing Scope Name [Slack] (#2641) 2022-05-02 16:21:11 -06:00
Peer Richelsen
c2d52bcfd2 consistency for tablet booking page (#2640) 2022-05-02 16:21:11 -06:00
Peer Richelsen
d1c37f84aa fixed layout in insalled apps (#2639) 2022-05-02 16:21:11 -06:00
sean-brydon
83f9defc65 Unlock edit on reschedule (#2628) 2022-05-02 16:21:11 -06:00
Joe Au-Yeung
c4dbab2637 Add new response if request contains account id (#2629)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-02 16:21:11 -06:00
alannnc
e76fafdccf Fix book event form schema validation (#2633) 2022-05-02 16:21:11 -06:00
sean-brydon
73e3e4e226 Adding validation for name and email (#2612) 2022-05-02 16:21:11 -06:00
Hariom Balhara
6535d654d7 Add Event Types Test (#2610)
* Add Event Types Test

* Accept license for tests

* Accept license on preview

* Remove debugging code

* Add License consent flag

* Test fixes

* Update playwright.config.ts

* Update webhookResponse-chromium.txt

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-05-02 16:21:11 -06:00
Hariom Balhara
a224a46654 Make sure that absolute URL is of WEBAPP only (#2624) 2022-05-02 16:21:11 -06:00
Peer Richelsen
59c0784cd6 Update PULL_REQUEST_TEMPLATE.md 2022-05-02 16:21:11 -06:00
Peer Richelsen
b544d5f781 Update PULL_REQUEST_TEMPLATE.md 2022-05-02 16:21:11 -06:00
Hariom Balhara
ebf1373339 Reduce Payload for Event-Types[Avoid 500] (#2627)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2022-05-02 16:21:11 -06:00
sean-brydon
75a07f527e Fix scope (#2625) 2022-05-02 16:21:11 -06:00
alannnc
4b75bf7cce Fix/login with provider (#2594) 2022-05-02 16:21:11 -06:00
Agusti Fernandez
e260ba0e49 Swagger docs improved (#2607)
* fix: adds servers in openapi, remove hack in snippets, update deps, make dynamic import to use latests swagger ui deps

* fix: remove unneded import

* fix: adds yarn dev commands for api and swagger

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-02 16:21:11 -06:00
sean-brydon
95dfb5b538 Loader Components (#2616)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-05-02 16:21:11 -06:00
sean-brydon
7d3f070e27 Bug/email notes hidden (#2611)
* Fix Width

* Fixes email notes

* Fixing reschedule email

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-05-02 16:21:11 -06:00
Hariom Balhara
01fdbaa990 Improve logs for 500 error in datadog for /book/event (#2593) 2022-05-02 16:21:11 -06:00
Hariom Balhara
05acd26efe Add debugging details (#2585) 2022-05-02 16:21:11 -06:00
sean-brydon
1421b9c0af Feat/impersonate users (#2503)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-05-02 16:21:11 -06:00
sean-brydon
6197ae25c6 Fix providerName (#2589)
Co-authored-by: Omar López <zomars@me.com>
2022-05-02 16:21:11 -06:00
zomars
de0883b14b v1.5.3 2022-05-02 16:07:45 -06:00
zomars
acc6db901c Migration seeder fixes 2022-05-02 16:02:45 -06:00
Joe Au-Yeung
7f463830bd Add Meta Mask to app store (#2650)
* Adds available apps

* Adds App Model

* WIP

* Create meta mask app folder

* Add description and images

* Remove credential from installed apps page

* Updates seeder script

* Seeder fixes

* lowercase categories

* Upgrades prisma

* WIP

* WIP

* Hopefully fixes circular deps

* Type fixes

* Fixes seeder

* Adds migration to connect Credentials to Apps

* Updates app store callbacks

* Updates google credentials

* Uses dirName from DB

* Type fixes

* Update reschedule.ts

* Seeder fixes

* Fixes categories listing

* Update index.ts

* Update schema.prisma

* Updates dependencies

* Renames giphy app

* Uses dynamic imports for app metadata

* Fixes credentials error

* Uses dynamic import for api handlers

* Dynamic import fixes

* Allows for simple folder names in app store

* Remove video adaptor

* Squashes app migrations

* seeder fixes

* Renames to metamask

* Updates metamask metadata

* Fixes dyamic imports

* Remove comments

* Create migration.sql

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-05-02 15:44:37 -06:00
Omar López
6a27fb2959 Fixes/app store keys in db (#2651)
* Adds available apps

* Adds App Model

* WIP

* Updates seeder script

* Seeder fixes

* lowercase categories

* Upgrades prisma

* WIP

* WIP

* Hopefully fixes circular deps

* Type fixes

* Fixes seeder

* Adds migration to connect Credentials to Apps

* Updates app store callbacks

* Updates google credentials

* Uses dirName from DB

* Type fixes

* Update reschedule.ts

* Seeder fixes

* Fixes categories listing

* Update index.ts

* Update schema.prisma

* Updates dependencies

* Renames giphy app

* Uses dynamic imports for app metadata

* Fixes credentials error

* Uses dynamic import for api handlers

* Dynamic import fixes

* Allows for simple folder names in app store

* Squashes app migrations

* seeder fixes

* Fixes dyamic imports

* Update apiHandlers.tsx
2022-05-02 14:39:35 -06:00
Shrey Gupta
21867c9cd4 feat(app-store): Add Giphy app (#2580) 2022-05-01 21:42:35 +01:00
Peer Richelsen
276821e0b5 Update README.md 2022-04-30 15:55:25 +02:00
Peer Richelsen
8028b1ddad Update index.mdx 2022-04-30 15:54:57 +02:00
Peer Richelsen
5abbd818d3 Update PULL_REQUEST_TEMPLATE.md 2022-04-30 14:51:23 +02:00
sean-brydon
43944a7d31 Enable Autocomplete (#2645)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-29 17:28:34 +00:00
Peer Richelsen
8bdc137917 added animations for dialog and tooltip (#2648)
* added animations for dialog and tooltip

* Update .env.example
2022-04-29 17:17:34 +00:00
Joe Au-Yeung
02fb15228b Hotfix - change calendar error message (#2643)
* Change calendar error message

* Change calendar error message

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-28 20:45:27 +00:00
Syed Ali Shahbaz
59a1db9068 Hash my url (#2484)
* disposable link model added

* disposable model updated

* added disposable slug availability page

* added disposable book page

* added disposable slug hook

* added disposable link booking flow

* updated schema

* checktype fix

* added checkfix and schema generated

* create link API added

* added one time link view on event type list

* adjusted schema

* fixed disposable visual indicator

* expired check and visual indicator added

* updated slug for disposable event type

* revised schema

* WIP

* revert desc

* revert --WIP

* rework based on change of plans

* further adjustments

* added eventtype option for hashed link

* added refresh and delete on update

* fixed update call conditions

* cleanup

* code improvement

* clean up

* Potential fix for 404

* backward compat for booking page

* fixes regular booking for user and team

* typefix

* updated path for Booking import

* checkfix

* e2e wip

* link err fix

* workaround for banner issue in event type update-test

* added regenerate hash check

* fixed test according to new testID

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-04-28 09:44:26 -06:00
alannnc
8e956893ca Fix emails and cal event descriptions (#2634)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-28 15:05:29 +00:00
sean-brydon
d960e03acf Fixes google meet url not appearing calendar invite (#2636)
* Update cal description

* Tidy up console logs

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-28 14:54:31 +00:00
Afzal Sayed
99666440cf Pass userId while creating event-type (#2599)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-28 07:56:10 -06:00
sean-brydon
f274c0bde3 Fixing Scope Name [Slack] (#2641) 2022-04-28 12:42:37 +00:00
Peer Richelsen
d1082e55a4 consistency for tablet booking page (#2640) 2022-04-28 13:31:55 +02:00
Peer Richelsen
af0d1980c6 fixed layout in insalled apps (#2639) 2022-04-28 13:22:40 +02:00
sean-brydon
a6183e0ccf Unlock edit on reschedule (#2628) 2022-04-28 09:10:40 +00:00
Joe Au-Yeung
eea40c69f7 Add new response if request contains account id (#2629)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-27 21:34:04 +00:00
alannnc
13ae773868 Fix book event form schema validation (#2633) 2022-04-27 23:21:18 +02:00
sean-brydon
6f0fcc9d1b Adding validation for name and email (#2612) 2022-04-27 16:19:04 +01:00
Hariom Balhara
7d98c0bb1c Add Event Types Test (#2610)
* Add Event Types Test

* Accept license for tests

* Accept license on preview

* Remove debugging code

* Add License consent flag

* Test fixes

* Update playwright.config.ts

* Update webhookResponse-chromium.txt

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-04-27 08:58:04 -06:00
Hariom Balhara
82d77dc10f Make sure that absolute URL is of WEBAPP only (#2624) 2022-04-27 16:28:36 +02:00
Peer Richelsen
ae1f35f515 Update PULL_REQUEST_TEMPLATE.md 2022-04-27 14:16:31 +02:00
Peer Richelsen
66f3fd2e07 Update PULL_REQUEST_TEMPLATE.md 2022-04-27 14:15:44 +02:00
Hariom Balhara
cf346f6aa3 Reduce Payload for Event-Types[Avoid 500] (#2627)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2022-04-27 11:08:13 +00:00
sean-brydon
34d3aac4b0 Fix scope (#2625) 2022-04-27 16:27:39 +05:30
alannnc
c22b6ca670 Fix/login with provider (#2594) 2022-04-26 15:12:08 +00:00
Agusti Fernandez
fa1b29a99f Swagger docs improved (#2607)
* fix: adds servers in openapi, remove hack in snippets, update deps, make dynamic import to use latests swagger ui deps

* fix: remove unneded import

* fix: adds yarn dev commands for api and swagger

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-26 15:00:53 +00:00
sean-brydon
d61238c832 Loader Components (#2616)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-26 14:11:02 +00:00
sean-brydon
28b432058a Bug/email notes hidden (#2611)
* Fix Width

* Fixes email notes

* Fixing reschedule email

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-26 13:46:20 +00:00
Hariom Balhara
4360ada3e4 Improve logs for 500 error in datadog for /book/event (#2593) 2022-04-26 11:31:57 +00:00
Hariom Balhara
5336bf3fe2 Add debugging details (#2585) 2022-04-26 11:15:57 +00:00
sean-brydon
6d5db1cb3a Feat/impersonate users (#2503)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-04-26 14:18:17 +05:30
sean-brydon
9fffaa20a2 Fix providerName (#2589)
Co-authored-by: Omar López <zomars@me.com>
2022-04-26 04:20:13 +00:00
sean-brydon
fd73a4ac92 Fix Width (#2587)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-26 02:48:36 +00:00
sean-brydon
29a6c70fc3 Heading Skeleton (#2601)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-26 02:38:41 +00:00
Afzal Sayed
96f6c644bd Improve create event-type flow (#2446)
* Improve create event-type flow

* Update form values when query changes

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-26 02:13:15 +00:00
sean-brydon
7c12bb1e20 Bug/i118n flicker (#2609)
* Fix Global Flicker

* Fixes Flicker + Null return

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-25 18:26:13 -06:00
Peer Richelsen
10e796f956 Revert "feat: disable select when loading (#2475)" (#2608)
This reverts commit 307b098f83.
2022-04-26 01:26:03 +02:00
sean-brydon
071077f2dc Fix Global Flicker (#2604) 2022-04-26 00:20:16 +02:00
sean-brydon
afe957674c Adding avatar skeleton loader (#2600) 2022-04-25 20:18:07 +00:00
Carlos Gabriel
307b098f83 feat: disable select when loading (#2475) 2022-04-25 20:30:15 +02:00
sean-brydon
95a793dd5a Skeleton Loaders Implementation (#2596)
* Skeleton Loaders

* Remove Href

* Fix Height Jumping around

* Subtle Colors

* feedback by ciaran

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-25 17:01:51 +00:00
sean-brydon
a0057911c1 Fix back button (#2592) 2022-04-25 10:32:01 +02:00
Hariom Balhara
93c75b5fef Embed Miscellaneous Improvements and Fixes (#2499) 2022-04-25 10:03:00 +05:30
Afzal Sayed
53d7e57142 Fix update event type authorization (#2588) 2022-04-24 15:02:04 -06:00
Hariom Balhara
2c4a891a89 Add debugging details (#2582) 2022-04-23 18:45:46 +00:00
Joe Au-Yeung
8e0c7759be Update README.md (#2578) 2022-04-22 21:43:59 +02:00
Peer Richelsen
41dc01ea3c Update next-i18next.config.js (#2568) 2022-04-22 20:40:40 +02:00
Syed Ali Shahbaz
9c985edb6b away user fix (#2576) 2022-04-22 16:03:01 +01:00
Bailey Pumfleet
69ef309cb5 Update website submodule 2022-04-21 22:31:02 +01:00
Omar López
f10bf38292 Removed deprecated JWT secrets (#2574) 2022-04-21 15:10:23 -06:00
Agusti Fernandez
02f68b104b fix: adds title to public-api docs (#2573) 2022-04-21 20:46:37 +00:00
Agusti Fernandez
8bc5a75249 Feature: Verify login on signup with magic link. (#2122)
* manual migration to rename verificationtoken, maybe it could be dropped and create a new table instead if we're not using it, will consult @zomars

* feat: rename verificationRequest --> verificationToken in schema.prisma

* fix: rename verificationRequest -> verificationToken in the codebase

* feat: add default cookies for next-auth

* fix: moves @lib/serverConfig to @calcom/lib so it can be called by website too

* fix: make self-certificate work in dev env by not rejecting tls in serverConfig

* fix verificationTokenToken typo

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

* Adds domain: .cal.com if not dev env in cookies

* Adds default-cookies to apps/web, and nextauth_domain to turbo website build deps"a

* update NEXTAUTH_DOMAIN to NEXTAUTH_COOKIE_DOMAIN

* Updates website submodule

* Removes deprecated env vars

* Consolidates auth logic in one place

* Updates website module

* Signup fixes

* Build fixes

* Updates example

* Updates example

* Fixes

* Fix Email Verification

* fix: move csrf-token cookiePrefix from __Host -> __Secure

* Removes console log

* Fixes link in email template

* Removed irrelevant coment

* Testing with a 32 bit secret

* Fixes for cookien in E2E

* E2E fixes

* Fixes Stripe tests locally

* Temp fix for E2E

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-04-21 14:32:25 -06:00
Joe Au-Yeung
97e4cca252 Remove global app installed button (#2561)
* Remove global app installed button

* Add Jitsi add button

* Find app credentials based on variant

* Make huddle installable

* Remove default installed message from installed apps page

* Display Jitsi and Huddle as locations if installed

* Reverse global app changes and made Jitsi and Huddle non global

* Changes to app page refrence #2556

* Fix type errors

* Revert code

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-21 12:17:56 -06:00
sean-brydon
18d41b52a2 Add chat public scope to slack scopes (#2564)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-21 19:04:38 +02:00
manuelgu
26c0f82edf fix: typo in slack app description (#2563)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-21 17:25:43 +02:00
SiderealArt
c12436afb0 Update Traditional Chinese translation (#2566)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-21 17:20:56 +02:00
milospuac
fead885aa4 Create slack.mdx (#2565)
Documentation for our Slack commands
2022-04-21 14:26:29 +01:00
sean-brydon
e680bb1548 Adding Scope for bot permissions (#2530)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-04-21 10:32:56 +01:00
alannnc
6e82d38249 Fix for appstore and wipemycal render (#2556)
* Fix for appstores and wipe-my-cal

* Fix email subject for reschedule

* Fix email subject for reschedule

* Fix api add wipemycal return

* Now we ask on a endpoint if app its installed

* Fix types

Co-authored-by: Omar López <zomars@me.com>
2022-04-20 23:37:25 +02:00
zomars
9f63299a1a Fixes hubspot metadata 2022-04-20 14:20:02 -06:00
zomars
702f31c935 Prepares prettier for local modules 2022-04-20 12:37:03 -06:00
zomars
08db282a07 Extracts EmptyScreen 2022-04-20 12:00:49 -06:00
zomars
080a394bb3 Renames package
# Conflicts:
#	package.json
2022-04-20 11:52:07 -06:00
zomars
8fb429e073 Renames package
# Conflicts:
#	package.json
2022-04-20 11:51:26 -06:00
Afzal Sayed
00a3ff89e4 Uniquely index event-type with teamId and slug (#2452) 2022-04-19 18:35:12 -06:00
Omar López
8f3b854559 Admin console (#2555)
* Adds admin submodule

* Adds admin submodule
2022-04-19 16:58:46 -06:00
Omar López
05edb144b2 Adds admin submodule (#2554) 2022-04-19 16:55:41 -06:00
Yassin Eldeeb
8c173c840b fix(web): overflow with auto scroll instead of scroll (#2535)
* fix(readme): update setup steps

* fix(readme): update required node version in prerequisites section

* fix(web): overflow-auto instead of overflow-scroll

* Revert "fix(readme): update setup steps"

This reverts commit 2938d510c874e9298b82465baeae5af9181c01d8.

* Revert "fix(readme): update required node version in prerequisites section"

This reverts commit 848e1a153538475acb42daa6ddbd74dd474611f5.

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-04-19 16:50:19 +01:00
Bailey Pumfleet
b540f44d6c Create CODE_OF_CONDUCT.md 2022-04-19 14:27:44 +01:00
Agusti Fernandez
7493093a1a fix: dangling zero in api keys list (#2548) 2022-04-19 12:32:17 +01:00
Agusti Fernandez
cf68541520 update swagger (#2547) 2022-04-19 10:50:40 +02:00
Peer Richelsen
b4ee4413cc Update eventTypes.tsx (#2545) 2022-04-18 22:40:56 +02:00
Peer Richelsen
f214830d0f Update README.mdx (#2544) 2022-04-18 22:08:49 +02:00
Peer Richelsen
c92070a5a2 Update package.json 2022-04-18 19:53:49 +02:00
Hariom Balhara
102ca5403d Fix "Edit Booking" Icon (#2536) 2022-04-18 11:25:56 +01:00
Hariom Balhara
7fd57b88dc Fix mobile reschedule actions (#2534) 2022-04-17 17:37:16 +02:00
Hariom Balhara
5f57694148 Fix Mobile UI for Booking Availability Page. Make it consistent with booking page (#2533) 2022-04-17 13:25:11 +02:00
Peer Richelsen
73c97e85d4 overflow scroll for categories (#2502) 2022-04-16 20:37:19 +00:00
Syed Ali Shahbaz
ccde0c20ab fixed avatargroup in mobile screens (#2498)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-16 22:23:15 +02:00
sean-brydon
d2d3c67144 Display most recent (#2529) 2022-04-16 13:51:44 +02:00
Agusti Fernandez
6d5af81f68 adds authorize to swagger docs to pass apiKey (#2524) 2022-04-16 08:22:22 +01:00
Hariom Balhara
2e9d4125ed Fix Embed Documentation (#2525) 2022-04-16 06:17:47 +00:00
Omar López
56c32beebc Update README.md 2022-04-15 21:02:35 -06:00
Agusti Fernandez
faa67e0bb6 Feature: Adds api keys to cal.com webapp (#2277)
* feat: add ApiKey model for new Api auth, owned by a user

* fix: remove metadata:Json and add note:String instead in new apiKey model

* fix: rename apiKey to apiKeys in moder User relation in schema.prisma

* feat: add hashedKey to apiKey and lastUsedAt datetime to keep track of usage of keys and makiung them securely stored in db

* fix 30 day -> 30 days in expiresAt

* feat: api keys frontend in security page

* adds hashedKey to api key model, add frontend api keys in security page

* Make frontend work to create api keys with or without expiry, note, defaults to 1 month expiry

* remove migration for now, add env.example to swagger, sync api

* feat: hashed api keys

* fix: minor refactor and cleanup in apiKeys generator

* add api key success modal

* sync apps/api

* feat: We have API Keys in Security =)

* remove swagger env from pr

* apps api sync

* remove comments in password section

* feat: migration for api keys schema

* sync api w main

* delete apps/api

* add back apps/api

* make min date and disabled optional props in datepicker

* feat fix type check errors

* fix : types

* fix: rmeove renaming of verificationrequest token indexes in migration

* fix: remove extra div

* Fixes for feedback in PR

* fix button />

* fix: rename weird naming of translation for you_will_only_view_it_once

* fix: remove ternary and use && to avoid null for false

* fix sync apps/api with main not old commit

* fix empty className

* fix: remove unused imports

* fix remove commented jsx fragment close

* fix rename editing

* improve translations

* feat: adds beta tag in security tab under api keys

* fix: use api keys everywhere

* fix: cleanup code in api keys

* fix: use watch and controller for neverexpires/datepicker

* Fixes: improve api key never expires

* add back change password h2 title section in security page

* fix update env API_KEY_ prefix default to cal_

* fix: improve eidt api keys modal

* fix: update edit mutation in viewer.apiKeys

* Update apps/web/ee/components/apiKeys/ApiKeyListItem.tsx

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

* fix: item: any to pass build

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-04-15 20:58:34 -06:00
Leo Giovanetti
ffebe8e901 HubSpot App (#2380)
* Initial changes

* OAuth done and credentials stored

* Added "other" integrations

* Switching to hubspot api client

* Event creation for all attendees

* Update and delete done

* Doc update

* Fixing types

* App label is not mandatory

* Fixing bad merge: App label deleted

* Fixing bad automerge

* Removing  c.log

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-15 23:23:38 -03:00
Omar López
2cafe2d98e Update README.md 2022-04-15 20:12:24 -06:00
Agusti Fernandez
d03038d976 Fix: styling (#2523)
* remove vercel favicon and logo, remove empty space on top due to Head

* adds a negative margin-top to compensate for svg-assets

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-16 01:58:07 +00:00
Agusti Fernandez
7e392da78a remove vercel favicon and logo, remove empty space on top due to Head (#2522)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
2022-04-16 03:35:49 +02:00
Agusti Fernandez
f8f3456b92 Adds iframe to developer.cal.com (swagger-ui) into docs/public-api.mdx (#2518)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
2022-04-15 23:56:32 +00:00
Agusti Fernandez
3b637eefaa adds redirect for /api to developer.cal.com (#2517)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
2022-04-15 14:19:52 -06:00
Agusti Fernandez
46e1d28881 improve styling on mobile for swagger docs (#2516)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-15 12:52:51 -06:00
Agusti Fernandez
f23cc8b99f Adds env example and changes route (#2515) 2022-04-15 19:11:35 +01:00
Hariom Balhara
6843347dd7 Embed: Bump Versions and update Readmes and descriptions (#2507) 2022-04-15 18:55:36 +01:00
Yassin Eldeeb
063d40aa0a fix(readme): specify nodejs version in prereq section (#2514) 2022-04-15 11:38:45 -06:00
Agusti Fernandez
bee5c83eed change hardcoed to use /api in route (#2513) 2022-04-15 18:30:47 +01:00
Hariom Balhara
8132b04a27 Fix Duplicat elocations (#2512) 2022-04-15 11:29:45 -06:00
sean-brydon
dabf5367bc DailyVideo default (#2505)
Co-authored-by: Omar López <zomars@me.com>
2022-04-15 17:07:37 +02:00
Agusti Fernandez
33287d6944 update docs to cal.dev (#2509) 2022-04-15 14:43:43 +01:00
Agusti Fernandez
f229bb6513 feat: adds swagger-ui new app in monorepo (#2375) 2022-04-15 13:56:22 +01:00
Agusti Fernandez
c16aabd9e8 sync api and website (#2504) 2022-04-15 13:06:38 +02:00
alannnc
c06d8164bc feature/app wipe my cal (#2496) 2022-04-15 10:09:40 +00:00
sean-brydon
080f2bb845 Updating App-Infomation + pics (#2489) 2022-04-15 10:35:50 +01:00
Peer Richelsen
25e4e28c2a small app store fixes (#2501)
* added new other illustration, fixed dialog styling

* revert color change
2022-04-15 08:54:09 +00:00
alannnc
5b90ace8cf feature/app wipe my cal (#2487)
* WIP bookings page ui changes, created api endpoint

* Ui changes mobile/desktop

* Added translations

* Fix lib import and common names

* WIP reschedule

* WIP

* Save wip

* [WIP] builder and class for CalendarEvent, email for attende

* update rescheduled emails, booking view and availability page view

* Working version reschedule

* Fix for req.user as array

* Added missing translation and refactor dialog to self component

* Test for reschedule

* update on types

* Update lib no required

* Update type on createBooking

* fix types

* remove preview stripe sub

* remove unused file

* remove unused import

* Fix reschedule test

* Refactor and cleaning up code

* Email reschedule title fixes

* Adding calendar delete and recreate placeholder of cancelled

* Add translation

* Removed logs, notes, fixed types

* Fixes process.env types

* Use strict compare

* Fixes type inference

* Type fixing is my middle name

* Update apps/web/components/booking/BookingListItem.tsx

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

* Update packages/core/builders/CalendarEvent/director.ts

* Update apps/web/pages/success.tsx

* Updates rescheduling labels

* Update packages/core/builders/CalendarEvent/builder.ts

* Type fixes

* Update packages/core/builders/CalendarEvent/builder.ts

* Only validating input blocked once

* E2E fixes

* Stripe tests fixes

* Wipe my cal init commit

* Fixes circular dependencies

* Added conditional display for wipe my cal button

* Added placeholder image for app category

* Fix type string for conditional validation

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: zomars <zomars@me.com>
2022-04-14 20:24:27 -06:00
Omar López
ba73960a02 App store metadata refactor (#2495)
* Delete api

* Fixes circular dependencies

* Revert "Delete api"

This reverts commit 174177a8331fc376b2dc7abda2c7adc56b24dd60.

* Delete _metadata.ts

* Update metadata.ts
2022-04-14 20:04:21 -06:00
buschco
94f64f9730 fix handling for recurring events (#2455) 2022-04-14 23:29:16 +01:00
Omar López
21d183e661 Fixes postinstall scripts (#2492)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-14 15:59:00 -06:00
Afzal Sayed
699d910ab4 Perf: Optimize event-types page (#2436)
* Perf: Optimize event-types page

* Memoize layout in Shell

* setQueryState without awaiting mutate for optimistic update

* Update Shell.tsx

* Fix types

* Update auth-index.test.ts

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-14 21:49:51 +00:00
alannnc
3c6ac395cc Feature/reschedule bookings (#2351)
* WIP bookings page ui changes, created api endpoint

* Ui changes mobile/desktop

* Added translations

* Fix lib import and common names

* WIP reschedule

* WIP

* Save wip

* [WIP] builder and class for CalendarEvent, email for attende

* update rescheduled emails, booking view and availability page view

* Working version reschedule

* Fix for req.user as array

* Added missing translation and refactor dialog to self component

* Test for reschedule

* update on types

* Update lib no required

* Update type on createBooking

* fix types

* remove preview stripe sub

* remove unused file

* remove unused import

* Fix reschedule test

* Refactor and cleaning up code

* Email reschedule title fixes

* Adding calendar delete and recreate placeholder of cancelled

* Add translation

* Removed logs, notes, fixed types

* Fixes process.env types

* Use strict compare

* Fixes type inference

* Type fixing is my middle name

* Update apps/web/components/booking/BookingListItem.tsx

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

* Update packages/core/builders/CalendarEvent/director.ts

* Update apps/web/pages/success.tsx

* Updates rescheduling labels

* Update packages/core/builders/CalendarEvent/builder.ts

* Type fixes

* Update packages/core/builders/CalendarEvent/builder.ts

* Only validating input blocked once

* E2E fixes

* Stripe tests fixes

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: zomars <zomars@me.com>
2022-04-14 15:25:24 -06:00
Omar López
6bb4b2e938 Fixes auth E2E tests (#2490) 2022-04-14 20:17:59 +00:00
Hariom Balhara
d1b063d59d User should be able to rebook a cancelled timeslot (#2483)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-04-14 16:01:38 +01:00
Clark Weckmann
cfbf419f57 Updated git link and Prisma command. (#2363)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-04-14 15:58:58 +01:00
Alex van Andel
5fdc5078cc Styling tweaks to inputs and Select (+ TimezoneSelect) (#2453)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-04-14 15:58:23 +01:00
Bailey Pumfleet
d91f667d0c Update greyscale palette 2022-04-14 15:50:00 +01:00
Hariom Balhara
9ed666a475 Add app-store tests (#2465)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-14 09:30:38 +05:30
Hariom Balhara
39935306fc Embed behind authentication page, Floating Button Popup and other changes (#2468)
Co-authored-by: Omar López <zomars@me.com>
2022-04-14 08:17:34 +05:30
Hariom Balhara
ce476bf90f Consider Pending/Accepted bookings only (#2479)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-04-13 10:22:27 -07:00
sean-brydon
b0d8eac2a2 Changing Daily to Cal.com (#2478) 2022-04-13 16:52:46 +02:00
Demian Caldelas
e1df207f5d Prepare for deprecated radix-ui IdProvider (#2068)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Omar López <zomars@me.com>
2022-04-12 18:41:42 +00:00
Hariom Balhara
75c2ccff96 RoundRobin Booking Error in a particular case (#2471)
* users can be array of user when roundrobin team booking is there with availablity of multiple people

* Return empty array

* Add comments

* checktype fix

* removed extra condition

Co-authored-by: Syed Ali Shahbaz <alishahbaz7@gmail.com>
2022-04-12 10:10:18 -07:00
Joe Au-Yeung
9d86039987 Remove to string, return dayjs object (#2464) 2022-04-12 13:01:50 +01:00
Omar López
cde131a351 Upgrades tRPC (#2460) 2022-04-12 09:32:14 +00:00
Hariom Balhara
31d1bde52a Prevent unauthorized update to schedule (#2466) 2022-04-12 10:22:29 +01:00
Afzal Sayed
4c5ae567e4 Introduce isPublic prop in Shell (#2437)
Co-authored-by: zomars <zomars@me.com>
2022-04-12 04:38:10 +00:00
zomars
a3e0d0aec9 Fixes for blank Shell 2022-04-11 08:54:56 -07:00
Omar López
95af0fb631 Revert "Revert "Fixes public apps page (#2422)" (#2461)" (#2462)
This reverts commit a5522c98a0.
2022-04-11 08:36:21 -07:00
Bailey Pumfleet
a5522c98a0 Revert "Fixes public apps page (#2422)" (#2461)
This reverts commit 81f3e824ff.
2022-04-11 16:11:35 +01:00
Omar López
81f3e824ff Fixes public apps page (#2422) 2022-04-11 15:21:20 +01:00
sean-brydon
61c60fc319 Add response events (#2456)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-10 17:19:45 +00:00
Omar López
b6da0f0553 Allows E2E to run from forks (#2423)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-10 18:19:06 +02:00
Alex van Andel
5e3da4d178 Check STRIPE_PRIVATE_KEY exists before stripe call during disband (#2454) 2022-04-10 08:57:08 -07:00
github-actions[bot]
66aeadffbb New Crowdin translations by Github Action (#2421)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-04-10 00:33:32 +02:00
sean-brydon
399f4978f8 Adding Username (#2424) 2022-04-09 15:40:02 +01:00
Peer Richelsen
0b9f6124e9 Update introduction.mdx 2022-04-09 12:01:37 +02:00
Hariom Balhara
df4a41127f Embed: Documentation TS errors Fix, IFrame Communication Tests, Updated documentation (#2432) 2022-04-08 16:59:08 +00:00
Syed Ali Shahbaz
eceba51020 Dynamic group booking follow-up (#2430) 2022-04-08 17:50:10 +01:00
Joe Au-Yeung
01eee52849 Updating contributing to app store docs (#2433)
* Allow any types in docs

* Update doc
2022-04-08 15:59:51 +01:00
Agusti Fernandez
385421d250 fix order of past bookings (#2425) 2022-04-08 12:50:00 +01:00
Hariom Balhara
c63d81719b Embed Improvements (#2365)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-08 05:33:24 +00:00
sean-brydon
96f6294542 Show eventDescription on email (#2371)
* Show eventDescription on email

* Fixing Inheritance

* Fixing Duplicate getLocation

* Type fixes

Co-authored-by: zomars <zomars@me.com>
2022-04-07 18:22:11 +00:00
Gwenaël Gallon
5e7d34b9c4 [fr] Add missing translations (#2409)
Update with last missing French translations

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-07 20:10:58 +02:00
github-actions[bot]
472b295c93 New Crowdin translations by Github Action (#2401)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-04-07 20:06:58 +02:00
Benedikt Hopmann
5577a60f26 Update german translation (#2415)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-07 20:03:52 +02:00
sean-brydon
fefc314d35 Fixing api points not returning (#2419)
Co-authored-by: Omar López <zomars@me.com>
2022-04-07 16:26:33 +00:00
Miguel Nieto A
06df6c9e91 Save event-type's currency on db (#2404)
* Save event-type's currency on db

* Add currency field to formMethods definition
2022-04-07 08:34:49 -07:00
Syed Ali Shahbaz
e6587efd27 Hotfix for merge loss in PR2416 (#2418) 2022-04-07 15:34:11 +01:00
Syed Ali Shahbaz
a551919152 Dynamic group link for more than 2 user fixed (#2416) 2022-04-07 14:41:26 +01:00
Hariom Balhara
173e7846e8 Fix team booking (#2417) 2022-04-07 13:51:16 +01:00
Syed Ali Shahbaz
63635fc110 Dynamic group booking fix to accept "+" in URL and updated Head SEO for dynamic booking (#2414) 2022-04-07 11:28:52 +01:00
Hariom Balhara
31a8f25bb6 Fix E2E Tests and Fix broken success page and non existing user URL (#2411) 2022-04-07 08:34:19 +01:00
Omar López
652b15c9e7 Adds huddle01 screenshots (#2406)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-06 13:21:08 -07:00
Omar López
f9bd93197e DB deploy fixes (#2408)
* DB deploy fixes

* Update turbo.json

* Turbo fixes
2022-04-06 12:52:48 -07:00
Omar López
0e93af912e Don't display uninstalled apps (#2405)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-06 10:51:31 -07:00
Syed Ali Shahbaz
d1ffd1edae dynamic group links (#2239)
* --init

* added default event types

* updated lib path

* updated group link design

* fixed collective description

* added default minimum booking notice

* Accept multi user query for a default event type

* check types

* check types --WIP

* check types still --WIP

* --WIP

* --WIP

* fixed single user type not working

* check fix

* --import path fix

* functional collective eventtype page

* fixed check type

* minor fixes and --WIP

* typefix

* custominput in defaultevent fix

* added booking page compatibility for dynamic group links

* added /book compatibility for dynamic group links

* checktype fix --WIP

* checktype fix

* Success page compatibility added

* added migrations

* added dynamic group booking slug to booking creation

* reschedule and database fix

* daily integration

* daily integration --locationtype fetch

* fixed reschedule

* added index to key parameter in eventtype list

* fix + added after last group slug

* added user setting option for dynamic booking

* changed defaultEvents location based on recent changes

* updated default event name in updated import

* disallow booking when one in group disallows it

* fixed setting checkbox association

* cleanup

* udded better error handling for disabled dynamic group bookings

* cleanup

* added tooltip to allow dynamic setting and enable by default

* Update yarn.lock

* Fix: Embed Fixes, UI configuration PRO Only, Tests (#2341)

* #2325 Followup (#2369)

* Adds initial MDX implementation for App Store pages

* Adds endpoint to serve app store static files

* Replaces zoom icon with dynamic-served one

* Fixes zoom icon

* Makes Slider reusable

* Adds gray-matter for MDX

* Adds zoom screenshots

* Update yarn.lock

* Slider improvements

* WIP

* Update TrendingAppsSlider.tsx

* WIP

* Adds MS teams screenshots

* Adds stripe screenshots

* Cleanup

* Update index.ts

* WIP

* Cleanup

* Cleanup

* Adds jitsi screenshot

* Adds Google meet screenshots

* Adds office 365 calendar screenshots

* Adds google calendar screenshots

* Follow #2325

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* requested changes

* further requested changes

* more changes

* type fix

* fixed prisma/client import path

* added e2e test

* test-fix

* E2E fixes

* Fixes circular dependency

* Fixed paid bookings seeder

* Added missing imports

* requested changes

* added username slugs as part of event description

* updated event description

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-06 10:20:30 -07:00
Afzal Sayed
d340ee62bb Fix login and onboarding flows (#2390)
* Fix login and onboarding flows

* Remove unused import

* Update apps/web/components/Shell.tsx

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* Prefer early return

* Prevent regressions

Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2022-04-06 17:07:22 +00:00
Hariom Balhara
c07b9b96fe Add translation for rediret string (#2399)
* Add translation for rediret string

* Simplify translations

* Update success.tsx

Co-authored-by: zomars <zomars@me.com>
2022-04-06 16:21:42 +00:00
Demian Caldelas
551892fa30 Refactor login tests (#2337)
Co-authored-by: Omar López <zomars@me.com>
2022-04-06 08:13:09 -07:00
Peer Richelsen
7fd65ceb8a added vietnamese (#2400) 2022-04-06 15:01:32 +02:00
github-actions[bot]
4fc8e2a2ac New Crowdin translations by Github Action (#2386)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-06 15:00:34 +02:00
Benedikt Hopmann
eebc1bce1a Update german translation (#2395) 2022-04-06 14:59:55 +02:00
sean-brydon
02dbb88e6b Slack App Integration (#2041)
* patch applied

* patch applied

* We shouldn't pollute global css

* Build fixes

* Updates typings

* WIP extracting zoom to package

* Revert "Upgrades next to 12.1 (#1895)" (#1903)

This reverts commit ede0e98e1f.

* Tweak/gitignore prisma zod (#1905)

* Extracts ignored createEventTypeBaseInput

* Adds postinstall script

* Revert "Tweak/gitignore prisma zod (#1905)" (#1906)

This reverts commit 15bfeb30d7.

* Eslint fixes (#1898)

* Eslint fixes

* Docs build fixes

* Upgrade to next 12.1 (#1904)

* Upgrades next to 12.1

* Fixes build

* Updaters e2e test pipelines

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Fix URL by removing slash and backslash (#1733)

* Fix URl by removing slash and backslash

* Implement slugify

* Add data type

* Fixing folder structure

* Solve zod-utils conflict

* Build fixes (#1929)

* Build fixes

* Fixes type error

* WIP

* Conflict fixes

* Removes unused file

* TODO

* WIP

* Type fixes

* Linting

* WIP

* Moved App definition to types

* WIP

* WIP

* WIP

* WIP WIP

* Renamed zoomvideo app

* Import fix

* Daily.co app (#2022)

* Daily.co app

* Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts

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

* Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts

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

* Missing deps for newly added contants to lib

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

* WIP

* WIP

* WIP

* Daily fixes

* Updated type info

* Slack Oauth integration - api route ideas

* Adds getLocationOptions

* Type fixes

* Adds location option for daily video

* Revert "Slack Oauth integration - api route ideas"

This reverts commit 35ffa78e929339c4badb98cdab4e4b953ecc7cca.

* Slack Oauth + verify sig

* Slack Oauth + verify sig

Implementing connect slack with workspace OAuth

Implemented the ability for slack to send requests on events (commands etc) - This only works if slacks signature matches with our signature

* Revert "Slack Oauth + verify sig"

This reverts commit ee95795e0f0ae6d06be4e0a423afb8c315d9af7d.

* WIP - Signature verifiaction failure

* Huddle01 migration to app store (#2038)

* Jitsi Video App migration

* Removing uneeded dependencies

* Missed unused reference

* Missing dependency

`@calcom/lib` is needed in the `locationOption.ts` file

* Huddle01 migration to app store

* WIP: PostData for creating event

* Optimising Query

Vital as we only have 3 seconds max to return the response to slack.

* Jitsi Video App migration (#2027)

* Jitsi Video App migration

* Removing uneeded dependencies

* Missed unused reference

* Missing dependency

`@calcom/lib` is needed in the `locationOption.ts` file

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

* Monorepo/app store MS Teams Integration (#2080)

* Create teamsvideo package

* Remove zoom specific refrences

* Add teams video files

* Rename to office365_video

* Add call back to add crednetial type office365_teams

* Rename to office_video to match type

* Add MS Teams as a location option

* Rename files

* Add teams reponse interface and create meeting

* Comment out Daily imports

* Add check for Teams integration

* Add token checking functions

* Change template to create event rather than meeting

* Add comment to test between create link and event

* Add teams URL to booking

* Ask for just onlineMeeting permission

* Add MS Teams logo

* Add message to have an enterprise account

* Remove comments

* Comment back hasDailyIntegration

* Comment back daily credentials

* Update link to MS Graph section of README

* Move API calls to package

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

* Re-adds missing module for transpiling

* Adding connect button if there is on user

* Adds email as required field for app store metadata

* WIP: migrates tandem to app store

* Cleanup

* Migrates tandem api routes to app store

* Fixes tandem api handlers

* Big WIP WIP

* Show todays bookings.

* No booking message to json

* Transition into modals

Better UX for submitting forms.

* Create Bookings - Working

* Fixing /today to show today and not all upcoming

* Fixing message

* Build fixes

* WIP

* Fixes annoying circular dependency bug

I've spent a whole day on this....

* Location option cleanup

* Type fixes

* Update EventManager.ts

* Update CalendarManager.ts

* Merge branch 'monorepo/app-store' into sean-monorepo-slack-oauth

* Moves CalendarService back to lib

* Moves apple calendar to App Store

* Cleanup

* Booking Success

* Merge branch 'main' into sean-monorepo-slack-oauth

* Restored moved file

* Delete TeamRole.tsx

* Undoing unrelated changes

* Cleanup

* Cleanup

* Updates website

* Delete .env.example

* Update yarn.lock

* Adds instructions to README

* Build fixes

* Uses generic app store api handler

* Adds install button and cleanup

* Updates .env.example

* Update README.md

* Renames slackapp to slackmessaing

* Update InstallAppButton.tsx

* Delete locationOption.ts

* Type fixes

* Build fixes

* Links + Fixing connection issue

* fixed merge conflict

* fixed merge conflict

* Type fixes

* Update index.ts

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Juan Esteban Nieto Cifuentes <89233604+Jenietoc@users.noreply.github.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2022-04-06 12:37:06 +00:00
Peer Richelsen
41755c8c90 fixed redirect banner for mobile (#2388)
* improved redirect banner on success page

* changed text for redirect input box

* nit

* Updates submodules

* Update yarn.lock

Co-authored-by: zomars <zomars@me.com>
2022-04-06 12:26:08 +00:00
alannnc
9d512e70c4 Fix with ical convert to zone (#2377)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-05 19:01:47 +00:00
Omar López
f8b7e17fda Fixes/locations (#2383)
* Moves locations to App Store and Core

* LocationType fixes

* Update App.d.ts

* Re-add Google Meet

* Moves location labels to the app store

* Renames labels to locationLabels

* Update utils.ts

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-05 18:03:22 +00:00
Peer Richelsen
4d58281d6f Update LICENSE 2022-04-05 18:37:55 +02:00
Omar López
6932d3600e Fix/post deploy migrations (#2385)
* Runs db migrations post-deploy

* Deploy fixes
2022-04-05 16:30:11 +00:00
Hariom Balhara
d76b9b0d01 Feature: Support redirecting to an external URL on successful booking (#2087) 2022-04-05 08:05:40 +00:00
Peer Richelsen
b3f9921dd8 Update BookingPage.tsx 2022-04-05 00:35:51 +02:00
Peer Richelsen
1e071126fe Update index.ts 2022-04-05 00:35:03 +02:00
Demian Caldelas
d7ce4fb983 Resync yarn.lock. Adding some peer deps for turbo packages/* (#2262)
* Resync yarn.lock & add peer deps

* Upgrades zod

Co-authored-by: zomars <zomars@me.com>
2022-04-04 16:23:42 -06:00
buschco
2d6cb1eb73 respect the timezone from the ical event (#2361)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-04 21:52:52 +00:00
Omar López
ffff59dd00 Attempt to fix Stripe webhooks (#2355)
Split the large transaction so we can debug better what's causing the error from Stripe dashboard
2022-04-04 14:39:59 -06:00
Omar López
a7f5250b4a Enables strict mode in nextjs apps (#2354)
- We need this so zod works correctly

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-04 20:26:14 +00:00
Omar López
4ff21deb89 #2325 Followup (#2369)
* Adds initial MDX implementation for App Store pages

* Adds endpoint to serve app store static files

* Replaces zoom icon with dynamic-served one

* Fixes zoom icon

* Makes Slider reusable

* Adds gray-matter for MDX

* Adds zoom screenshots

* Update yarn.lock

* Slider improvements

* WIP

* Update TrendingAppsSlider.tsx

* WIP

* Adds MS teams screenshots

* Adds stripe screenshots

* Cleanup

* Update index.ts

* WIP

* Cleanup

* Cleanup

* Adds jitsi screenshot

* Adds Google meet screenshots

* Adds office 365 calendar screenshots

* Adds google calendar screenshots

* Follow #2325

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-04 15:53:17 +00:00
Hariom Balhara
5138c676b1 Fix: Embed Fixes, UI configuration PRO Only, Tests (#2341) 2022-04-04 15:44:04 +00:00
Agusti Fernandez
7c08e946c6 update api submodule (#2367)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
2022-04-04 17:19:24 +02:00
Omar López
5dbb60dc85 App store mdx support (#2325)
* Adds initial MDX implementation for App Store pages

* Adds endpoint to serve app store static files

* Replaces zoom icon with dynamic-served one

* Fixes zoom icon

* Makes Slider reusable

* Adds gray-matter for MDX

* Adds zoom screenshots

* Update yarn.lock

* Slider improvements

* WIP

* Update TrendingAppsSlider.tsx

* WIP

* Adds MS teams screenshots

* Adds stripe screenshots

* Cleanup

* Update index.ts

* WIP

* Cleanup

* Cleanup

* Adds jitsi screenshot

* Adds Google meet screenshots

* Adds office 365 calendar screenshots

* Adds google calendar screenshots

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-04 15:21:33 +01:00
Peer Richelsen
26e76df6c8 fixed avatar issue on mobile team pages, improved readability of dark mode text on booking page (#2364) 2022-04-04 10:20:49 +00:00
github-actions[bot]
c094d05913 New Crowdin translations by Github Action (#2302)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-04-04 11:14:57 +02:00
Joe Au-Yeung
9bbaf1a7fa Add MS Teams install button (#2362)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-04 09:11:24 +00:00
Peer Richelsen
3fc49a8cee Update README.md 2022-04-04 10:46:02 +02:00
sean-brydon
95aa5fe308 Fix - Email Notes (#2356)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-04-03 11:59:40 -06:00
Peer Richelsen
9948f9d854 show event-type headline even if description is empty (#2359) 2022-04-03 13:15:31 +02:00
Alex van Andel
279b4d57f1 Turn off formatOnSave and remove eslint.run & fix CalendarManager lint (#2357) 2022-04-03 00:43:28 +01:00
Omar López
966a5f30ec Adds missing confirm email after payment (#2353) 2022-04-02 15:30:07 +01:00
Omar López
d997aef4f8 updating submodules (#2350)
Co-authored-by: github-actions <github-actions@github.com>
2022-04-01 12:15:44 -07:00
Colin Griffin
be6ca25f08 update submodule to use https and specify branch (#2346)
Co-authored-by: Omar López <zomars@me.com>
2022-04-01 11:35:48 -07:00
Omar López
f2436d2a04 Urgent fix for team bookings (#2352) 2022-04-01 11:04:29 -07:00
Omar López
2d055327c2 updating submodules (#2345) 2022-04-01 14:54:44 +01:00
Syed Ali Shahbaz
b33a3d5652 adds a fallback email to allow fallback gravatar (#2339) 2022-04-01 12:07:58 +02:00
Omar López
f71c0ddfc3 Fixes locations options mistranslations (#2336) 2022-03-31 14:29:03 -07:00
sean-brydon
f293f8b5c4 Bug - link not showing (#2333)
* Bug - link not showing

* Added where location as link on emails

* Location URL test fixes

* URLs validator fixes

Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-03-31 19:57:12 +00:00
Miguel Nieto A
0494fccb8e Set a default for "create events on" (#2215)
* fix: 🐛 Set a default for create events on

* fix: 🐛 Save default value to db

* Revert fixes in frontend

* fix: 🐛 Set a default for "create events on" from backend"

* fix: 🐛 Update frontend when destinationCalendar is disconnected
2022-03-31 10:26:26 -07:00
Hariom Balhara
4a58da62d6 [Feature]Booking Embed (#2227) 2022-03-31 09:45:47 +01:00
Omar López
4e9c3be598 Submodule update (#2324)
* Updates submodules

* Submodule updates
2022-03-30 13:13:40 -07:00
Omar López
6b32f03027 Submodule sync (#2323)
* Create submodule-sync.yml

* Update submodule-sync.yml

* Update submodule-sync.yml

* Update submodule-sync.yml

* Update submodule-sync.yml

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-03-30 13:11:09 -07:00
Omar López
4e102d8b30 Submodule sync (#2322)
* Create submodule-sync.yml

* Update submodule-sync.yml

* Update submodule-sync.yml

* Update submodule-sync.yml

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-03-30 13:07:10 -07:00
Omar López
d89271759a Submodule sync (#2321)
* Create submodule-sync.yml

* Update submodule-sync.yml

* Update submodule-sync.yml

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-03-30 12:51:08 -07:00
Omar López
63800492a9 Submodule sync (#2320)
* Create submodule-sync.yml

* Update submodule-sync.yml

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-03-30 12:49:53 -07:00
Omar López
c58e3791d1 Create submodule-sync.yml (#2319)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-03-30 12:37:11 -07:00
Peer Richelsen
ef97f4115d made app store category grid flexible based on amount of categories (#2315)
* made app store category grid flexible based on amount of categories

* Update yarn.lock

Co-authored-by: zomars <zomars@me.com>
2022-03-30 17:33:04 +00:00
sean-brydon
6b0e8db496 Adding checkbox to toggle hideNotes (#2310) 2022-03-29 09:59:22 -07:00
Peer Richelsen
80af5dd236 added more illustrations for new categories (#2304)
* added more illustrations for new categories

* minor changes to messaging
2022-03-28 14:34:14 -07:00
Demian Caldelas
0390ae9ee1 Introducing Playwright Fixtures - Users Factory (#2293)
* Fix not able to logout using the logout path

* Add users fixture for e2e tests

* typo

Co-authored-by: Omar López <zomars@me.com>
2022-03-28 20:06:41 +00:00
Omar López
2d2df2d4db Attempt to fix linting for external contributors (#2303) 2022-03-28 12:21:31 -07:00
buschco
cc5537dd1f improve date query parsing (#1992) (#2296)
Co-authored-by: Omar López <zomars@me.com>
2022-03-28 11:50:31 -07:00
sean-brydon
bd66ca183f Feat - disable notes (#2249)
* Feat - disable notes

Stops notes from showing in calendar when checked.

* Removing index rename

* Update apps/web/lib/integrations/calendar/interfaces/Calendar.ts

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

* Rename to hideCalendarNotes

* Update schema.prisma

* Update webhookResponse-chromium.txt

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-03-28 18:07:13 +00:00
github-actions[bot]
4f1a380969 New Crowdin translations by Github Action (#2298)
* New Crowdin translations by Github Action

* Update common.json

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-03-28 15:07:35 +02:00
sean-brydon
4ca5bd58ee Fixing Booking day name (#2297)
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-03-27 21:48:13 +02:00
Alex van Andel
b1d804405b Implements copy day functionality into Availability (#2273) 2022-03-27 21:13:27 +02:00
Peer Richelsen
14ba410352 fixed dark mode for booking page and input (#2292) 2022-03-27 00:40:41 +01:00
sean-brydon
b0bb894e1a Submodule change (#2289) 2022-03-26 14:19:44 -07:00
Omar López
83a395bf55 Updates submodules (#2288) 2022-03-25 17:57:10 -07:00
Omar López
ec58a9dd70 The Dotenv Refactor (#2275)
* dotenv refactoring

* dotenv fixes

* Env variables cleanup

* Updates e2e variables

* Moves environment file to types

* Removes conflicting configs

* Readds missing variables

* Fixes

* More fixes

* Update .env.example

* Update yarn.lock

* Update turbo.json

* Fixes e2e

* Temp fix

* disables cache for lint

* Please work

* I'm getting desperate here.

* Matches node versions

* Take 2

* Revert "Take 2"

This reverts commit a735f47f2325c2040168e0cd99dea0fc357a791f.

* Update .env.example
2022-03-25 17:39:38 -07:00
Peer Richelsen
2d7e1ccc05 added categories index (#2286) 2022-03-25 15:44:27 -07:00
github-actions[bot]
7490f07a32 New Crowdin translations by Github Action (#2256)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-03-25 23:20:19 +01:00
Peer Richelsen
6c62918c1f iframe button text right (#2285) 2022-03-25 23:15:16 +01:00
Peer Richelsen
897d255676 fixed layout shift, added new payments illustration (#2283)
* fixed layout shift, added new payments illustration

* undo integrations bg
2022-03-25 23:09:07 +01:00
Peer Richelsen
1b8132eb2f added app descriptions (#2281) 2022-03-25 15:03:10 -07:00
Peer Richelsen
49bb80eeb4 added illustrations to app store (#2280)
* fixed a ton of app store layout shift bugs

* added illustrations

* Sync submodules to main

* Update yarn.lock

Co-authored-by: zomars <zomars@me.com>
2022-03-25 19:46:21 +00:00
Peer Richelsen
2104624633 fixed a app store layout shift bugs (#2279)
* fixed a ton of app store layout shift bugs

* Sync submodules to main

* Update yarn.lock

Co-authored-by: zomars <zomars@me.com>
2022-03-25 19:23:03 +00:00
Omar López
94006156d7 Adds sort order to free event type 404 (#2278) 2022-03-25 19:50:13 +01:00
alannnc
92534c7e6d fix reacreate of bookingReferences when rescheduling (#2272)
Co-authored-by: Omar López <zomars@me.com>
2022-03-24 23:29:32 +00:00
sean-brydon
296697370d Improving A11y across pages (#2219)
* Improving Event-Types A11y

* Fixing more A11y Issues

* Removing Yarn.lock
2022-03-24 20:14:16 +00:00
zomars
37a10a9638 Submodules update 2022-03-24 13:07:39 -07:00
Omar López
05a7babd56 Fixes/app store mobile (#2271)
* Grid fixes for mobile

* Adds missing NavTabs to installed apps page
2022-03-24 19:46:36 +00:00
alannnc
3341074bb2 Fix/login username registration (#2241)
* username update from getting-started when received as query param

* Added test for onboarding username update

* Now saving username saved in localStorage

* remove username field

* Removed wordlist

* Implement checkoutUsername as api endpoint

* Remove unused lib utils not empty

Co-authored-by: zomars <zomars@me.com>
2022-03-24 10:45:56 -07:00
Hariom Balhara
1a77e4046e Add Booking tests - Reschedule and Cancel. (#2163)
Co-authored-by: zomars <zomars@me.com>
2022-03-24 10:32:28 -07:00
Omar López
367da36660 Add index rename migration (#2162)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-03-24 09:53:15 -07:00
Peer Richelsen
552751ffcf app store fixes (#2266)
* temporally hiding reviews and stars for apps, removed nuke my cal

* fixed categories headline and back button

* truncate description

* updated huddle app

* Delete truncate.ts

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-03-24 09:51:37 -07:00
Omar López
785b156f95 Fixes overflow for long calendar names (#2261)
- Adds title attribute for overflowed text

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-03-24 09:43:07 -07:00
Peer Richelsen
a15b93c276 made dark mode much more consistent and fixed a ton of dark mode problems with teams (#2258) 2022-03-24 14:15:24 +01:00
Omar López
767d1fb186 Prevents crash when sending emails (#2264)
* Quick fixes to core libs

* Prevents crash when sending emails
2022-03-23 20:47:50 -07:00
Omar López
f7a2e1e7ac Quick fixes to core libs (#2263) 2022-03-23 20:29:27 -07:00
Omar López
1a27edd462 Booking page improvements (#2260)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-03-24 02:27:35 +00:00
Omar López
87dcdec044 Add various location events to seeder (#2259)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-03-24 02:13:04 +00:00
Alex van Andel
e1964553c4 Bugfix/amend schedule reload (#2254)
* Invalidate onSuccess delete instead of reload

* Added schedule name to availability update + fix update invalidation

Co-authored-by: Omar López <zomars@me.com>
2022-03-23 23:23:18 +00:00
Omar López
f536d1040c App Store (#1869)
* patch applied

* patch applied

* We shouldn't pollute global css

* Build fixes

* Updates typings

* WIP extracting zoom to package

* Revert "Upgrades next to 12.1 (#1895)" (#1903)

This reverts commit ede0e98e1f.

* Tweak/gitignore prisma zod (#1905)

* Extracts ignored createEventTypeBaseInput

* Adds postinstall script

* Revert "Tweak/gitignore prisma zod (#1905)" (#1906)

This reverts commit 15bfeb30d7.

* Eslint fixes (#1898)

* Eslint fixes

* Docs build fixes

* Upgrade to next 12.1 (#1904)

* Upgrades next to 12.1

* Fixes build

* Updaters e2e test pipelines

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Fix URL by removing slash and backslash (#1733)

* Fix URl by removing slash and backslash

* Implement slugify

* Add data type

* Fixing folder structure

* Solve zod-utils conflict

* Build fixes (#1929)

* Build fixes

* Fixes type error

* WIP

* Conflict fixes

* Removes unused file

* TODO

* WIP

* Type fixes

* Linting

* WIP

* Moved App definition to types

* WIP

* WIP

* WIP

* WIP WIP

* Renamed zoomvideo app

* Import fix

* Daily.co app (#2022)

* Daily.co app

* Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts

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

* Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts

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

* Missing deps for newly added contants to lib

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

* WIP

* WIP

* WIP

* Daily fixes

* Updated type info

* Slack Oauth integration - api route ideas

* Adds getLocationOptions

* Type fixes

* Adds location option for daily video

* Revert "Slack Oauth integration - api route ideas"

This reverts commit 35ffa78e929339c4badb98cdab4e4b953ecc7cca.

* Slack Oauth + verify sig

* Revert "Slack Oauth + verify sig"

This reverts commit ee95795e0f0ae6d06be4e0a423afb8c315d9af7d.

* Huddle01 migration to app store (#2038)

* Jitsi Video App migration

* Removing uneeded dependencies

* Missed unused reference

* Missing dependency

`@calcom/lib` is needed in the `locationOption.ts` file

* Huddle01 migration to app store

* Jitsi Video App migration (#2027)

* Jitsi Video App migration

* Removing uneeded dependencies

* Missed unused reference

* Missing dependency

`@calcom/lib` is needed in the `locationOption.ts` file

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

* Monorepo/app store MS Teams Integration (#2080)

* Create teamsvideo package

* Remove zoom specific refrences

* Add teams video files

* Rename to office365_video

* Add call back to add crednetial type office365_teams

* Rename to office_video to match type

* Add MS Teams as a location option

* Rename files

* Add teams reponse interface and create meeting

* Comment out Daily imports

* Add check for Teams integration

* Add token checking functions

* Change template to create event rather than meeting

* Add comment to test between create link and event

* Add teams URL to booking

* Ask for just onlineMeeting permission

* Add MS Teams logo

* Add message to have an enterprise account

* Remove comments

* Comment back hasDailyIntegration

* Comment back daily credentials

* Update link to MS Graph section of README

* Move API calls to package

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

* Re-adds missing module for transpiling

* Adds email as required field for app store metadata

* WIP: migrates tandem to app store

* Cleanup

* Migrates tandem api routes to app store

* Fixes tandem api handlers

* Big WIP WIP

* Build fixes

* WIP

* Fixes annoying circular dependency bug

I've spent a whole day on this....

* Location option cleanup

* Type fixes

* Update EventManager.ts

* Update CalendarManager.ts

* Moves CalendarService back to lib

* Moves apple calendar to App Store

* Cleanup

* More cleanup

* Migrates apple calendar

* Returns all connected calendars credentials

* No tsx needed in calcom/lib

* Update auth.ts

* Reordering

* Update i18n.utils.ts

* WIP: Google Meet

* Type fixes

* Type fixes

* Cleanup

* Update LinkIconButton.tsx

* Update TrialBanner.tsx

* Cleanup

* Cleanup

* Type fixes

* Update _appRegistry.ts

* Update fonts.css

* Update CalEventParser.ts

* Delete yarn.lock.rej

* Update eslint-preset.js

* Delete zoom.tsx

* Type fixes

* Migrates caldav to app store

* Cleanup

* Type fixes

* Adds caldav to app store

* Test fixes

* Updates integration tests

* Moar test fixes

* Redirection fixes

* Redirection fixes

* Update timeFormat.ts

* Update booking-pages.test.ts

* Connect button fixes

* Fix empty item

* Cal fixes andrea (#2234)

* Fixes #2178

* Fixes #2178

* Update apps/web/components/availability/Schedule.tsx

* Update apps/web/components/availability/Schedule.tsx

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

* added meta viewport to disable zoom on input focus on mobile (#2238)

* Update lint.yml (#2211)

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

* Fix prisma client bundle makes app slow (#2237)

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

* Slider fixes

* Removed unused code

* Full Shell when unauthed

* App sidebar responsive fixes

* Adds dynamic install button

* Fix for duplicate connected calendars

* Various fixes

* Display notification on app delete

* Reuse connect button

* Adds CalDav button

* Deprecates ConnectIntegration

* Simplify install button

* Adds Google Calendar connect button

* Adds Office 365 Install button

* Migrates Stripe to App Store

* Zoom Install Button (#2244)

* Fix minor css, app image load from static path

* Fix app logos remote img src (#2252)

* Adds missing exports

* Cleanup

* Disables install button for globally enabled apps

* Update EventManager.ts

* Stripe fixes

* Disables example app

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Juan Esteban Nieto Cifuentes <89233604+Jenietoc@users.noreply.github.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Sean Brydon <seanbrydon.me@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: andreaestefania12 <andreaestefania12@hotmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Demian Caldelas <denik.works@protonmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
2022-03-23 15:00:30 -07:00
Omar López
caeb2412de Update api (#2251) 2022-03-23 10:53:22 -07:00
Alex van Andel
77266535e5 Truthy value was impossible on completedOnboarding, either undefined/false (#2243) 2022-03-22 12:43:57 -07:00
Demian Caldelas
7716b4c15f Fix prisma client bundle makes app slow (#2237)
Co-authored-by: Omar López <zomars@me.com>
2022-03-22 15:22:20 +00:00
Omar López
91f13122eb Update lint.yml (#2211)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-03-22 07:52:47 -07:00
Peer Richelsen
3c967ab280 added meta viewport to disable zoom on input focus on mobile (#2238) 2022-03-22 11:03:31 +00:00
andreaestefania12
9f2e71beae Cal fixes andrea (#2234)
* Fixes #2178

* Fixes #2178

* Update apps/web/components/availability/Schedule.tsx

* Update apps/web/components/availability/Schedule.tsx

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2022-03-22 10:34:36 +00:00
Peer Richelsen
ab2542501a fixed onboarding (#2233) 2022-03-21 23:14:00 +00:00
Miguel Nieto A
6ed945943a feat: Show dialog only for InPerson and Link locations (#2206) 2022-03-21 20:45:53 +00:00
github-actions[bot]
b8980ced8e New Crowdin translations by Github Action (#2218)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-03-21 19:09:01 +00:00
Syed Ali Shahbaz
8671255d5c fix for email (#2220) 2022-03-21 10:21:00 +00:00
Leo Giovanetti
94b210329b Height didn't matched (#2216) 2022-03-20 09:48:11 +00:00
Alex van Andel
a8e137a55c Show button even when no event types (#2214) 2022-03-18 21:46:04 +00:00
Omar López
91a6e199a1 Adds team members to seeder (#2208)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-03-18 20:28:12 +00:00
Vlad
46f515a19f fixed Select component ':active' option color (#2207)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-03-18 18:31:30 +00:00
Peer Richelsen
47ce2feb3c minor layout improvements (#2209) 2022-03-18 14:22:56 -04:00
Demian Caldelas
ae27601405 Fix double forward slash in auth callback (#2197)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-03-18 10:56:56 -07:00
Peer Richelsen
94f6c80d57 added helpscout (#2204)
* added helpscout

* nit

* moved all help apps into its own component

* added helpscout to .env.example
2022-03-18 15:09:13 +00:00
sean-brydon
3c845cb226 Update Website A11ly (#2200) 2022-03-18 11:24:44 +00:00
github-actions[bot]
ea72ecc9e5 New Crowdin translations by Github Action (#2194)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-03-18 09:58:13 +00:00
Syed Ali Shahbaz
e59d29a429 reverts to before dialog change state (#2195) 2022-03-18 09:55:02 +00:00
alannnc
c558c880f2 Revert dialog view change (#2193) 2022-03-17 23:28:43 +00:00
Peer Richelsen
77879bc193 moved availability outside of advanced settigns (#2188)
* moved availability outside of advanced settigns

* added icon for availability and divider

* nit

* fixed padding availability on mobile

* nit

* nit
2022-03-17 20:49:20 +00:00
github-actions[bot]
1b813b0ee3 New Crowdin translations by Github Action (#2184)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-03-17 19:57:15 +00:00
Demian Caldelas
55587e92c1 Fix a set of E2E bugs causing several CI failures (#2177)
* Fix E2E bugs causing CI failutes

* Revert setup in dx

Co-authored-by: zomars <zomars@me.com>
2022-03-17 12:36:11 -07:00
Alex van Andel
39d395bf62 Reverted to pre-dialog content (#2189) 2022-03-17 19:21:47 +00:00
Peer Richelsen
95f92cac28 added emptyscreen component to availability and event-types (#2187)
* added emptyscreen component to availability

* added emptyscreen for event-types too

* added placeholder for adding new schedule

* didnt realise we had a Working Hours string alredy

* nit

* remove white background from empty availability
2022-03-17 19:13:39 +00:00
Omar López
deffb77875 Trigger missing deploy (#2186) 2022-03-17 10:07:33 -07:00
Alex van Andel
6a211dd5b3 Feature/multiple schedules post turbo (#2150)
* Concluded merge

* Applied stash to newly merged

* Always disconnect + remove redundant success message

* Added named dialog to replace new=1

* Merged with main p2

* Set eventTypeId to @unique

* WIP

* Undo vscode changes

* Availability dropdown works

* Remove console.log + set schedule to null as it is unneeded

* Added schedule to availability endpoint

* Reduce one refresh; hotfix state inconsistency with forced refresh for now

* Add missing translations

* Fixed some type errors I missed

* Ditch outdated remnant from before packages/prisma

* Remove Availability section for teams

* Bringing back the Availability section temporarily to teams to allow configuration

* Migrated getting-started to new availability system + updated translations + updated seed

* Fixed type error coming from main

* Titlecase 'default' by providing translation

* Fixed broken 'radio' buttons.

* schedule deleted translation added

* Added empty state for when no schedules are configured

* Added correct created message + hotfix reload hard on delete to refresh state

* Removed index renames

* Type fixes

* Update NewScheduleButton.tsx

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-03-17 09:48:23 -07:00
Syed Ali Shahbaz
bcbf8390e0 Refactor old dialog to radix Dialog (#2151)
* --init

* refactored more dialogs --WIP

* more modals replaced by dialogs --WIP

* fix for new dialog location import

* --WIP

* lint fix

* final dialog refactor

* added more width to max-w for dialog in sm screen

* clean-up

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-03-17 13:20:49 +00:00
github-actions[bot]
df64af2aba New Crowdin translations by Github Action (#2179)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-03-17 12:57:48 +00:00
github-actions[bot]
39ecf914ed New Crowdin translations by Github Action (#2165)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-03-17 12:46:34 +00:00
Omar López
82ab6f7a5b Update submodules (#2174) 2022-03-16 18:23:13 -07:00
Peer Richelsen
faa74dae39 more consistency for dropdown (#2173)
Co-authored-by: zomars <zomars@me.com>
2022-03-16 18:09:17 -07:00
Omar López
558897fe53 Migrates components and libs to packages (#2172)
* Migrates Dialog to ui package

* Migrates Alert to ui

* Migrate Button to ui

* UI and lib migrations

* Add missing imports

* Update Error.tsx
2022-03-16 16:36:43 -07:00
sean-brydon
f9f856d7ea Fixing Ghost Input (#2161)
* Fixing Ghost Input

* Removing form import

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-03-16 14:04:12 -07:00
Leo Giovanetti
6e4f8e67b6 Event type dropdown (#2081) 2022-03-16 19:55:18 +00:00
Omar López
c9484172a4 Re-enables tailwind purge (#2171) 2022-03-16 12:40:47 -07:00
alannnc
fdc99b346a fix/handle-premium-on-google-signup (#2160)
* checking if username is premium on google sign up

* Removed test validating query input value, but it's no longer required

* undo code that was moved to a function for reuse

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-03-16 19:33:20 +00:00
Omar López
06cec35522 Skip SAML tests if disabled (#2169)
* Temporarily disables forgot password test

* Skip SAML tests if disabled
2022-03-16 09:51:46 -07:00
Omar López
18c21d9b97 Temporarily disables forgot password test (#2168) 2022-03-16 09:40:56 -07:00
Agusti Fernandez
e94594d0b1 fixes prefill adding name in session and jwt (#2167) 2022-03-16 08:11:21 -07:00
Agusti Fernandez
e21813ba96 hotfix prefill undefined bug booking page (#2166)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
2022-03-16 13:18:11 +00:00
alannnc
c8505cd71c update SAML setup links for readme files (#2159)
* update setup links for readme files

* Fix file path

* Update apps/web/.env.example

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Omar López <zomars@me.com>
2022-03-16 11:40:58 +00:00
909 changed files with 38930 additions and 9232 deletions

84
.env.appStore.example Normal file
View File

@@ -0,0 +1,84 @@
# ********** INDEX **********
#
# - APP STORE
# - DAILY.CO VIDEO
# - GOOGLE CALENDAR/MEET/LOGIN
# - HUBSPOT
# - OFFICE 365
# - SLACK
# - STRIPE
# - TANDEM
# - ZOOM
# - GIPHY
# - VITAL
# - APP STORE **********************************************************************************************
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
# - DAILY.CO VIDEO
DAILY_API_KEY=
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='{}'
# To enable Login with Google you need to:
# 1. Set `GOOGLE_API_CREDENTIALS` above
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
GOOGLE_LOGIN_ENABLED=false
# - HUBSPOT
# Used for the HubSpot integration
# @see https://github.com/calcom/cal.com/#obtaining-hubspot-client-id-and-secret
HUBSPOT_CLIENT_ID=""
HUBSPOT_CLIENT_SECRET=""
# - OFFICE 365
# Used for the Office 365 / Outlook.com Calendar / MS Teams integration
# @see https://github.com/calcom/cal.com/#Obtaining-Microsoft-Graph-Client-ID-and-Secret
MS_GRAPH_CLIENT_ID=
MS_GRAPH_CLIENT_SECRET=
# - SLACK
# @see https://github.com/calcom/cal.com/#obtaining-slack-client-id-and-secret-and-signing-secret
SLACK_SIGNING_SECRET=
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
# - STRIPE
NEXT_PUBLIC_STRIPE_PUBLIC_KEY= # pk_test_...
STRIPE_PRIVATE_KEY= # sk_test_...
STRIPE_WEBHOOK_SECRET= # whsec_...
STRIPE_CLIENT_ID= # ca_...
PAYMENT_FEE_FIXED=10 # Take 10 additional cents commission
PAYMENT_FEE_PERCENTAGE=0.005 # Take 0.5% commission
# - TANDEM
# Used for the Tandem integration -- contact support@tandem.chat for API access.
TANDEM_CLIENT_ID=""
TANDEM_CLIENT_SECRET=""
TANDEM_BASE_URL="https://tandem.chat"
# - ZOOM
# Used for the Zoom integration
# @see https://github.com/calcom/cal.com/#obtaining-zoom-client-id-and-secret
ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET=
# - GIPHY
# Used for the Giphy integration
# @see https://support.giphy.com/hc/en-us/articles/360020283431-Request-A-GIPHY-API-Key
GIPHY_API_KEY=
# - VITAL
# Used for the vital integration
# @see https://github.com/calcom/cal.com/#obtaining-vital-api-keys
VITAL_API_KEY=
VITAL_WEBHOOK_SECRET=
# "sandbox" | "prod" | "production" | "development"
VITAL_DEVELOPMENT_MODE="sandbox"
# "us" | "eu"
VITAL_REGION="us"
# *********************************************************************************************************

View File

@@ -1,2 +1,108 @@
# It now lives at `apps/web/.env.example`
# DATABASE_URL got moved to `packages/prisma/.env.example`
# ********** INDEX **********
#
# - LICENSE
# - DATABASE
# - SHARED
# - NEXTAUTH
# - E-MAIL SETTINGS
# - LICENSE *************************************************************************************************
# Set this value to 'agree' to accept our license:
# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
#
# Summary of terms:
# - The codebase has to stay open source, whether it was modified or not
# - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
NEXT_PUBLIC_LICENSE_CONSENT=''
# ***********************************************************************************************************
# - DATABASE ************************************************************************************************
# ⚠️ ⚠️ ⚠️ DATABASE_URL got moved to `packages/prisma/.env.example` ⚠️ ⚠️ ⚠️
# ***********************************************************************************************************
# - SHARED **************************************************************************************************
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
# Change to 'http://localhost:3001' if running the website simultaneously
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
# To enable SAML login, set both these variables
# @see https://github.com/calcom/cal.com/tree/main/packages/ee#setting-up-saml-login
# SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml"
SAML_DATABASE_URL=
# SAML_ADMINS='pro@example.com'
SAML_ADMINS=
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
# PGSSLMODE='no-verify'
PGSSLMODE=
# - NEXTAUTH
# @see: https://github.com/calendso/calendso/issues/263
# @see: https://next-auth.js.org/configuration/options#nextauth_url
# Required for Vercel hosting - set NEXTAUTH_URL to equal your NEXT_PUBLIC_WEBAPP_URL
# NEXTAUTH_URL='http://localhost:3000'
NEXTAUTH_URL=
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
# You can use: `openssl rand -base64 32` to generate one
NEXTAUTH_SECRET=
# Used for cross-domain cookie authentication
NEXTAUTH_COOKIE_DOMAIN=.example.com
# Remove this var if you don't want Cal to collect anonymous usage
NEXT_PUBLIC_TELEMETRY_KEY=js.2pvs2bbpqq1zxna97wcml.oi2jzirnbj1ev4tc57c5r
# ApiKey for cronjobs
CRON_API_KEY='0cc0e6c35519bba620c9360cfe3e68d0'
# Application Key for symmetric encryption and decryption
# must be 32 bytes for AES256 encryption algorithm
# You can use: `openssl rand -base64 24` to generate one
CALENDSO_ENCRYPTION_KEY=
# Intercom Config
NEXT_PUBLIC_INTERCOM_APP_ID=
# Zendesk Config
NEXT_PUBLIC_ZENDESK_KEY=
# Help Scout Config
NEXT_PUBLIC_HELPSCOUT_KEY=
# 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=
# Use for internal Public API Keys and optional
API_KEY_PREFIX=cal_
# ***********************************************************************************************************
# - E-MAIL SETTINGS *****************************************************************************************
# Cal uses nodemailer (@see https://nodemailer.com/about/) to provide email sending. As such we are trying to
# allow access to the nodemailer transports from the .env file. E-mail templates are accessible within lib/emails/
# Configures the global From: header whilst sending emails.
EMAIL_FROM='notifications@yourselfhostedcal.com'
# Configure SMTP settings (@see https://nodemailer.com/smtp/).
# Note: The below configuration for Office 365 has been verified to work.
EMAIL_SERVER_HOST='smtp.office365.com'
EMAIL_SERVER_PORT=587
EMAIL_SERVER_USER='<office365_emailAddress>'
# Keep in mind that if you have 2FA enabled, you will need to provision an App Password.
EMAIL_SERVER_PASSWORD='<office365_password>'
# The following configuration for Gmail has been verified to work.
# EMAIL_SERVER_HOST='smtp.gmail.com'
# EMAIL_SERVER_PORT=465
# EMAIL_SERVER_USER='<gmail_emailAddress>'
## You will need to provision an App Password.
## @see https://support.google.com/accounts/answer/185833
# EMAIL_SERVER_PASSWORD='<gmail_app_password>'
# **********************************************************************************************************

View File

@@ -1,2 +0,0 @@
node_modules
packages/prisma/zod

View File

@@ -4,6 +4,10 @@
Fixes # (issue)
<!-- Please provide a loom video for visual changes to speed up reviews
Loom Video: https://www.loom.com/
-->
## Type of change
<!-- Please delete options that are not relevant. -->

View File

@@ -6,7 +6,6 @@ on:
jobs:
types:
name: Check types
strategy:
matrix:
node: ["14.x"]

View File

@@ -1,19 +1,25 @@
name: E2E test
on:
pull_request_target:
pull_request_target: # So we can test on forks
branches:
- main
paths-ignore:
- public/static/locales/**
jobs:
test:
timeout-minutes: 10
timeout-minutes: 20
name: Testing ${{ matrix.node }} and ${{ matrix.os }}
strategy:
matrix:
node: ["14.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
BASE_URL: http://localhost:3000
JWT_SECRET: secret
PLAYWRIGHT_SECRET: ${{ secrets.CI_PLAYWRIGHT_SECRET }}
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
@@ -26,12 +32,13 @@ jobs:
PAYMENT_FEE_FIXED: 10
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
SAML_ADMINS: pro@example.com
# NEXTAUTH_URL: xxx
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 }}
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
@@ -44,25 +51,20 @@ jobs:
POSTGRES_DB: calendso
ports:
- 5432:5432
runs-on: ${{ matrix.os }}
strategy:
matrix:
node: ["14.x"]
os: [ubuntu-latest]
steps:
- name: Checkout repo
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
fetch-depth: 2
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v2
with:
cache: "yarn"
cache-dependency-path: yarn.lock
node-version: ${{ matrix.node }}
# cache: "yarn"
# cache-dependency-path: yarn.lock
- name: Turbo Cache
id: turbo-cache

View File

@@ -1,23 +1,29 @@
name: Lint
on:
pull_request:
pull_request_target:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node: ["14.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Checkout repo
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: Use Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 14.x
cache: "yarn"
cache-dependency-path: yarn.lock
node-version: ${{ matrix.node }}
# cache: "yarn"
# cache-dependency-path: yarn.lock
- name: Install deps
if: steps.yarn-cache.outputs.cache-hit != 'true'

21
.github/workflows/submodule-sync.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Submodule Sync
on:
schedule:
- cron: "15 */4 * * *"
workflow_dispatch: ~
jobs:
submodule-sync:
name: Submodule update
runs-on: ubuntu-latest
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"

11
.gitignore vendored
View File

@@ -11,11 +11,11 @@ node_modules
# testing
coverage
/test-results/
playwright/videos
playwright/screenshots
playwright/artifacts
playwright/results
playwright/reports/*
**/playwright/videos
**/playwright/screenshots
**/playwright/artifacts
**/playwright/results
**/playwright/reports/*
# next.js
.next/
@@ -39,6 +39,7 @@ yarn-error.log*
.env.production.local
.env.*
!.env.example
!.env.appStore.example
# vercel
.vercel

10
.gitmodules vendored
View File

@@ -1,6 +1,12 @@
[submodule "apps/admin"]
path = apps/admin
url = https://github.com/calcom/admin.git
branch = main
[submodule "apps/api"]
path = apps/api
url = git@github.com:calcom/api.git
url = https://github.com/calcom/api.git
branch = main
[submodule "apps/website"]
path = apps/website
url = git@github.com:calcom/website.git
url = https://github.com/calcom/website.git
branch = main

View File

@@ -1,11 +1,9 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
// Auto-fix issues with ESLint when you save code changes
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.run": "onSave",
"typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"],
"spellright.documentTypes": ["markdown"]

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
bailey@cal.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

127
README.md
View File

@@ -33,7 +33,7 @@
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=social"></a>
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
</p>
<!-- ABOUT THE PROJECT -->
@@ -80,7 +80,7 @@ To get a local copy up and running, please follow these simple steps.
Here is what you need to be able to run Cal.
- Node.js
- Node.js (Version: >=14.x <15)
- PostgreSQL
- Yarn _(recommended)_
@@ -90,7 +90,7 @@ Here is what you need to be able to run Cal.
### Setup
1. Clone the repo
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))
```sh
git clone https://github.com/calcom/cal.com.git
@@ -102,19 +102,14 @@ Here is what you need to be able to run Cal.
cd cal.com
```
1. Copy `apps/web/.env.example` to `apps/web/.env`
```sh
cp apps/web/.env.example apps/web/.env
cp packages/prisma/.env.example packages/prisma/.env
```
1. Install packages with yarn
```sh
yarn
```
1. Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the .env file.
#### Quick start with `yarn dx`
> - **Requires Docker and Docker Compose to be installed**
@@ -126,10 +121,10 @@ yarn dx
#### Development tip
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `apps/web/.env` to get logging information for all the queries and mutations driven by **trpc**.
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **trpc**.
```sh
echo 'NEXT_PUBLIC_DEBUG=1' >> apps/web/.env
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
```
#### Manual setup
@@ -195,11 +190,11 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> apps/web/.env
### E2E-Testing
Be sure to set the environment variable `NEXTAUTH_URL` to the correct value. If you are running locally, as the documentation within `.env.example` mentions, the value should be `http://localhost:3000`.
```sh
# In first terminal
yarn dx
# In second terminal
yarn workspace @calcom/web test-e2e
# In a terminal just run:
yarn test-e2e
# To open last HTML report run:
yarn workspace @calcom/web playwright-report
@@ -213,7 +208,13 @@ yarn workspace @calcom/web playwright-report
git pull
```
2. Apply database migrations by running <b>one of</b> the following commands:
1. Check if dependencies got added/updated/removed
```sh
yarn
```
1. Apply database migrations by running <b>one of</b> the following commands:
In a development environment, run:
@@ -229,16 +230,13 @@ yarn workspace @calcom/web playwright-report
yarn workspace @calcom/prisma db-deploy
```
3. Check the `.env.example` and compare it to your current `.env` file. In case there are any fields not present
in your current `.env`, add them there.
1. Check for `.env` variables changes
For the current version, especially check if the variable `BASE_URL` is present and properly set in your environment, for example:
```
BASE_URL='https://yourdomain.com'
```sh
yarn predev
```
4. Start the server. In a development environment, just do:
1. Start the server. In a development environment, just do:
```sh
yarn dev
@@ -251,7 +249,7 @@ yarn workspace @calcom/web playwright-report
yarn start
```
5. Enjoy the new version.
1. Enjoy the new version.
<!-- DEPLOYMENT -->
## Deployment
@@ -317,6 +315,57 @@ We have a list of [good first issues](https://github.com/calcom/cal.com/labels/
5. Use **Application (client) ID** as the **MS_GRAPH_CLIENT_ID** attribute value in .env
6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attribute
### Obtaining Slack Client ID and Secret and Signing Secret
To test this you will need to create a Slack app for yourself on [their apps website](https://api.slack.com/apps).
Copy and paste the app manifest below into the setting on your slack app. Be sure to replace `YOUR_DOMAIN` with your own domain or your proxy host if you're testing locally.
<details>
<summary>App Manifest</summary>
```yaml
display_information:
name: Cal.com Slack
features:
bot_user:
display_name: Cal.com Slack
always_online: false
slash_commands:
- command: /create-event
url: https://YOUR_DOMAIN/api/integrations/slackmessaging/commandHandler
description: Create an event within Cal!
should_escape: false
- command: /today
url: https://YOUR_DOMAIN/api/integrations/slackmessaging/commandHandler
description: View all your bookings for today
should_escape: false
oauth_config:
redirect_urls:
- https://YOUR_DOMAIN/api/integrations/slackmessaging/callback
scopes:
bot:
- chat:write
- commands
- chat:write.public
settings:
interactivity:
is_enabled: true
request_url: https://YOUR_DOMAIN/api/integrations/slackmessaging/interactiveHandler
message_menu_options_url: https://YOUR_DOMAIN/api/integrations/slackmessaging/interactiveHandler
org_deploy_enabled: false
socket_mode_enabled: false
token_rotation_enabled: false
```
</details>
Add the integration as normal - slack app - add. Follow the oauth flow to add it to a server.
Next make sure you have your app running `yarn dx`. Then in the slack chat type one of these commands: `/create-event` or `/today`
> NOTE: Next you will need to setup a proxy server like [ngrok](https://ngrok.com/) to allow your local host machine to be hosted on a public https server.
### Obtaining Zoom Client ID and Secret
1. Open [Zoom Marketplace](https://marketplace.zoom.us/) and sign in with your Zoom account.
@@ -341,6 +390,30 @@ We have a list of [good first issues](https://github.com/calcom/cal.com/labels/
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
5. If you have the [Daily Scale Plan](https://www.daily.co/pricing) set the `DAILY_SCALE_PLAN` variable to `true` in order to use features like video recording.
### Obtaining HubSpot Client ID and Secret
1. Open [HubSpot Developer](https://developer.hubspot.com/) and sign into your account, or create a new one.
2. From within the home of the Developer account page, go to "Manage apps".
3. Click "Create app" button top right.
4. Fill in any information you want in the "App info" tab
5. Go to tab "Auth"
6. Now copy the Client ID and Client Secret to your .env file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspot othercalendar/callback` replacing Cal.com URL with the URI at which your application runs.
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scope called `crm.objects.contacts`
9. Click the "Save" button at the bottom footer.
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
### Obtaining Vital API Keys
1. Open [Vital](https://tryvital.io/) and click Get API Keys.
1. Create a team with the team name you desire
1. Head to the configuration section on the sidebar of the dashboard
1. Click on API keys and you'll find your sandbox `api_key`.
1. Copy your `api_key` to `VITAL_API_KEY` in the .env.appStore file.
1. Open [Vital Webhooks](https://app.tryvital.io/team/{team_id}/webhooks) and add `<CALCOM BASE URL>/api/integrations/vital/webhook` as webhook for connected applications.
1. Select all events for the webhook you interested, e.g. `sleep_created`
1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file.
<!-- LICENSE -->
## License
@@ -360,7 +433,7 @@ Special thanks to these amazing projects which help power Cal.com:
- [Day.js](https://day.js.org/)
- [Tailwind CSS](https://tailwindcss.com/)
- [Prisma](https://prisma.io/)
[<img src="https://jitsu.com/img/powered-by-jitsu.png?gh=true">](https://jitsu.com/?utm_source=cal.com-gihub)
<a href="https://jitsu.com/?utm_source=cal.com-gihub"><img height="40px" src="https://jitsu.com/img/powered-by-jitsu.png?gh=true" alt="Jitsu.com"></a>
Cal.com is an [open startup](https://jitsu.com) and [Jitsu](https://github.com/jitsucom/jitsu) (an open-source Segment alternative) helps us to track most of the usage metrics.

View File

@@ -18,7 +18,7 @@
"description": "Application Key for symmetric encryption and decryption. Must be 32 bytes for AES256 encryption algorithm.",
"value": "secret"
},
"JWT_SECRET": "secret"
"NEXTAUTH_SECRET": "secret"
},
"scripts": {
"postdeploy": "cd packages/prisma && npx prisma migrate deploy"

1
apps/admin Submodule

Submodule apps/admin added at 943cd10de1

View File

@@ -5,7 +5,7 @@
</a>
<a href="https://cal.com">Website</a>
·
<a href="https://github.com/calcom/docs/issues">Community Support</a>
<a href="https://github.com/calcom/cal.com/issues">Community Support</a>
</div>
# Cal.com Documentation

View File

@@ -0,0 +1,21 @@
function getAnchor(text) {
return text
.toLowerCase()
.replace(/[^a-z0-9 ]/g, "")
.replace(/[ ]/g, "-")
.replace(/ /g, "%20");
}
export default function Anchor({ as, children }) {
const anchor = getAnchor(children);
const link = `#${anchor}`;
const Component = as || "div";
return (
<Component id={anchor}>
<a href={link} className="anchor-link">
§
</a>
{children}
</Component>
);
}

View File

@@ -0,0 +1,33 @@
import { useState, useEffect } from "react";
// Define general type for useWindowSize hook, which includes width and height
export interface Size {
width: number | undefined;
height: number | undefined;
}
// Hook from: https://usehooks.com/useWindowSize/
export function useWindowSize(): Size {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState<Size>({
width: undefined,
height: undefined,
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}

View File

@@ -3,4 +3,14 @@ const withNextra = require("nextra")({
themeConfig: "./theme.config.js",
unstable_staticImage: true,
});
module.exports = withNextra();
module.exports = withNextra({
async rewrites() {
return [
// This redirects requests recieved at /api to /public-api to workaround nextjs default use of /api.
{
source: "/api",
destination: "/public-api",
},
];
},
});

View File

@@ -7,6 +7,7 @@
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "PORT=4000 next",
"lint": "next lint",
"type-check": "tsc --pretty --noEmit",
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
"start": "PORT=4000 next start",
"build": "next build"
@@ -14,6 +15,7 @@
"author": "Cal.com, Inc.",
"license": "MIT",
"dependencies": {
"iframe-resizer-react": "^1.1.0",
"next": "^12.1.0",
"nextra": "^1.1.0",
"nextra-theme-docs": "^1.2.2",

View File

@@ -1,7 +1,8 @@
import { AppProps } from "next/app";
import "nextra-theme-docs/style.css";
import "./style.css";
export default function Nextra({ Component, pageProps }) {
export default function Nextra({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}

View File

@@ -12,20 +12,26 @@ All apps can be found under `packages/app-store`. In this folder is `_example` w
```sh
├──_example
| ├──index.ts
| ├──package.json
| ├──.env.example
|
| ├──api
| | ├──example.ts
| | ├──index.ts
|
| ├──components
| | ├──InstallAppButton.tsx
| | ├──index.ts
|
| ├──lib
| | ├──adaptor.ts
| | ├──index.ts
|
| ├──static
| | ├──icon.svg
|
| ├──index.ts
| ├──package.json
| ├──.env.example
| ├──README.mdx
```
## Getting Started
@@ -38,8 +44,17 @@ In `index.js` fill out the meta data that will be rendered on the app page. Unde
Under the `/api` folder, this is where any API calls that are associated with your app will be handled. Since cal.com uses Next.js we use dynamic API routes. In this example if we want to hit `/api/example.ts` the route would be `{BASE_URL}/api/integrations/_example/example`. Export your endpoints in an `index.ts` file under `/api` folder and import them in your main `index.ts` file.
Under the `/components` folder, this is where the install button for your app should live. Follow the template under `_example` to add your on click action (ex. Redirecting to a log in page or opening a modal).
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
The `/static` folder is where your assets live.
On the app store page you can customize your apps description by adding a markdown file called `README.mdx`. If you do not add one then the description from you `package.json` will be used instead.
The `/static` folder is where you can store your app icon and any images that your `README.mdx` may use.
## Adding Your App to the App Store
To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object.
Under `packages/app-store/components.tsx`, in the `InstallAppButtonMap` object dynamically import your install button. Your install button should live under `{your_app}/components`.
If you need any help feel free to join us on [Slack](https://cal.com/slack)

View File

@@ -12,6 +12,6 @@ This is also the home of our design system documentation and developer docs.
If you don't already know what Cal.com is about, please head over to [our website](https://cal.com), where you can learn more about the product before venturing into the documentation.
Want to help make these docs even better? This site is fully open source, and the source code is available on [GitHub](https://github.com/calcom/docs). You can also click the edit button at the bottom of any page to start editing the source code and start a pull request.
Want to help make these docs even better? This site is fully open source, and the source code is available on [GitHub](https://github.com/calcom/cal.com/tree/main/apps/docs). You can also click the edit button at the bottom of any page to start editing the source code and start a pull request.
<Bleed>![Telemedicine Example](https://cal.com/telemedicine-example.svg)</Bleed>

View File

@@ -0,0 +1,262 @@
---
title: Embed
---
import Anchor from "../../components/Anchor"
# Embed
The Embed allows your website visitors to book a meeting with you directly from your website.
## Install on any website
Install the following Vanilla JS Snippet to get embed to work on any website. After that you can <a href="#popular-ways-in-which-you-can-embed-on-your-website">choose any of the ways</a> to show your Cal Link embedded on your website.
```html
<script>
(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
};
let d = C.document;
C.Cal =
C.Cal ||
function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement("script")).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar);
return;
}
p(cal, ar);
};
})(window, "https://cal.com/embed.js", "init");
Cal("init")
</script>
```
## Install with a Framework
### embed-react
It provides a react component `<Cal>` that can be used to show the embed inline at that place.
```bash
yarn add @calcom/embed-react
```
### Any XYZ Framework
You can use Vanilla JS Snippet to install
<Anchor as="H2">Popular ways in which you can embed on your website</Anchor>
Assuming that you have followed the steps of installing and initializing the snippet, you can show the embed in following ways:
### Inline
Show the embed inline inside a container element. It would take the width and height of the container element.
<details>
<summary>_Vanilla JS_</summary>
```html
<script>
Cal("inline", {
elementOrSelector: "Your Embed Container Selector Path", // You can also provide an element directly
calLink: "jane", // The link that you want to embed. It would open https://cal.com/jane in embed
config: {
name: "John Doe", // Prefill Name
email: "johndoe@gmail.com", // Prefill Email
notes: "Test Meeting", // Prefill Notes
guests: ["janedoe@gmail.com", "test@gmail.com"], // Prefill Guests
theme: "dark", // "dark" or "light" theme
},
});
</script>
*Sample sandbox*
```
<iframe src="https://codesandbox.io/embed/vanilla-js-inline-embed-r27n67?fontsize=14&hidenavigation=1&theme=dark"
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
title="Cal Component - Embed Inline Demo[React][TypeScript]"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
</details>
####
<details>
<summary>_React_</summary>
```jsx
import Cal from "@calcom/embed-react";
const MyComponent = () => (
<Cal
calLink="pro"
config={{
name: "John Doe",
email: "johndoe@gmail.com",
notes: "Test Meeting",
guests: ["janedoe@gmail.com"],
theme: "dark",
}}
/>
);
```
*Sample sandbox*
<iframe src="https://codesandbox.io/embed/cal-component-embed-inline-demo-react-typescript-d1zlcn?fontsize=14&hidenavigation=1&theme=dark"
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
title="Cal Component - Embed Inline Demo[React][TypeScript]"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
</details>
### Popup on any existing element
To show the embed as a popup on clicking an element, add `data-cal-link` attribute to the element.
<details>
<summary>Vanilla JS</summary>
To show the embed as a popup on clicking an element, simply add `data-cal-link` attribute to the element.
*Sample sandbox*
<iframe src="https://codesandbox.io/embed/popup-on-click-of-an-existing-element-y9lcuo?fontsize=14&hidenavigation=1&theme=dark"
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
title="Cal Component - Embed Inline Demo[React][TypeScript]"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
<button data-cal-link="jane" data-cal-config="A valid config JSON"></button>
</details>
<details>
<summary>React</summary>
```jsx
import "@calcom/embed-react";
const MyComponent = ()=> {
return <button data-cal-link="jane" data-cal-config='A valid config JSON'></button>
}
```
*Sample sandbox*
<iframe src="https://codesandbox.io/embed/embed-popup-on-click-of-an-existing-element-demo-react-sc967e?fontsize=14&hidenavigation=1&theme=dark"
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
title="Cal Component - Embed Inline Demo[React][TypeScript]"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
</details>
### Floating pop-up button
```html
<script>
Cal("floatingButton", {
// The link that you want to embed. It would open https://cal.com/jane in embed
calLink: "jane",
});
</script>
```
*Sample sandbox*
<iframe src="https://codesandbox.io/embed/embed-floating-button-popup-all-websites-cg7pru?fontsize=14&hidenavigation=1&theme=dark"
style={{width:"100%", height:"500px", border:0, borderRadius: "4px", overflow:"hidden"}}
title="Cal Component - Embed Inline Demo[React][TypeScript]"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
## Supported Instructions
Consider an instruction as a function with that name and that would be called with the given arguments.
### `inline`
Appends embed inline as the child of the element.
```html
<script>
Cal("inline", { elementOrSelector, calLink });
</script>
````
- `elementOrSelector` - Give it either a valid CSS selector or an HTMLElement instance directly
- `calLink` - Cal Link that you want to embed e.g. john. Just give the username. No need to give the full URL [https://cal.com/john](). It makes it easy to configure the calendar host once and use as many links you want with just usernames
### `ui`
Configure UI for embed. Make it look part of your webpage.
```html
<script>
Cal("inline", { styles });
</script>
```
- `styles` - It supports styling for `body` and `eventTypeListItem`. Right now we support just background on these two.
### preload
Usage:
If you want to open cal link on some action. Make it pop open instantly by preloading it.
```html
<script>
Cal("preload", { calLink });
</script>
```
- `calLink` - Cal Link that you want to embed e.g. john. Just give the username. No need to give the full URL [https://cal.com/john]()
## Actions
You can listen to an action that occurs in embedded cal link as follows. You can think of them as DOM events. We are avoiding the term "events" to not confuse it with Cal Events.
```html
<script>
Cal("on", {
action: "ANY_ACTION_NAME",
callback: (e)=>{
// `data` is properties for the event.
// `type` is the name of the action(You can also call it type of the action.) This would be same as "ANY_ACTION_NAME" except when ANY_ACTION_NAME="*" which listens to all the events.
// `namespace` tells you the Cal namespace for which the event is fired/
const {data, type, namespace} = e.detail;
}
})
</script>
```
Following are the list of supported actions.
-
| Action | Description | Properties |
|----------------------|------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| eventTypeSelected | When user chooses an event-type from the listing. | eventType:object // Event Type that has been selected" |
| bookingSuccessful | When the booking is successfully done. It might not be confirmed. | confirmed: boolean; //Whether confirmation from organizer is pending or not <br/><br/>eventType: "Object for Event Type that has been booked"; <br/><br/>date: string; // Date of Event <br/><br/>duration: number; //Duration of booked Event <br/><br/>organizer: object //Organizer details like name, timezone, email |
| linkReady | Tells that the link is ready to be shown now. | None |
| linkFailed | Fired if link fails to load | code: number; // Error Code <br/><br/>msg: string; //Human Readable msg <br/><br/>data: object // More details to debug the error |
| __iframeReady | It is fired when the embedded iframe is ready to communicate with parent snippet. This is mostly for internal use by Embed Snippet | None |
| __windowLoadComplete | Tells that window load for iframe is complete | None |
| __dimensionChanged | Tells that dimensions of the content inside the iframe changed. | iframeWidth:number, iframeHeight:number |
_Actions that start with __ are internal._

View File

@@ -5,17 +5,17 @@ title: Introduction
# Integrations
## Connecting new calendars
1. Go to the [Cal App Store](https://app.cal.com/integrations).
1. Go to the [Cal App Store](https://app.cal.com/apps).
2. Located at the top right of the screen, press the button saying '+ Connect A New App'
3. Choose the account your calendar is connected too by clicking 'Add'. (e.g. Google, Office 365, Zoom)
4. You will be redirected to the log in page of the chosen account.
5. Allow Cal access to view and edit your calendars.
6. You will be sent back to the [Cal App Store](https://app.cal.com/integrations). From here you will now be able to see your connected calendar!
6. You will be sent back to the [Cal App Store](https://app.cal.com/apps/installed). From here you will now be able to see your connected calendar!
## How to choose the primary Calendar?
If you have two or more integrated calendars and you want your events to show in only one, you can define a primary calendar like this:
1. Go to your [Integrations](https://app.cal.com/integrations) page.
1. Go to your [Installed](https://app.cal.com/apps/installed) page.
2. Next to your `Calendars` you will see a dropdown that says `Create events on:`.
3. Select your primary calendar.

View File

@@ -0,0 +1,21 @@
---
Title: Slack
---
# Slack
## Connecting
Connecting the bot is easy - If you are a workspace admin, the install button will add the bot to the workspace and also authorize your account with the bot. If you are a normal user, the install button will connect your Slack account with Cal.com. This will allow you to perform commands in Slack.
## Commands
`/today` - This command will display all meetings you have in your Cal.com profile for the current day. This will send a hidden message (not visible to anyone other than you) to the channel you issued the command in.
`/create-event` - It will display a modal allowing you to simply create a meeting invite with anyone in Slack. Success/Error information will be displayed in a private direct message from the bot.
`/links` - This command will post all your Cal.com meeting links into the current Slack channel you are in. **Note**: The bot needs to have permission to talk in the channel you are sending the message in. Otherwise, you won't be able to send your links.
As this is the beggining stage of our Slack integration, we plan on adding more commands in the future that will further improve your Cal.com experience.
## Self-Hosted
If you are using our self-hosted version, please refer to our documentation in
[cal.com/README.md](https://github.com/calcom/cal.com/blob/main/README.md#obtaining-slack-client-id-and-secret-and-signing-secret)

View File

@@ -6,6 +6,7 @@
"event-types": "Event Types",
"teams": "Teams",
"integrations": "Integrations",
"public-api": "API",
"webhooks": "Webhooks",
"settings": "Settings",
"import": "Import",

View File

@@ -0,0 +1,20 @@
import Bleed from 'nextra-theme-docs/bleed'
import Head from "next/head";
import IframeResizer from "iframe-resizer-react";
import {useWindowSize} from "../lib/useWindowSize";
<Bleed full>
<Head><title>Public API | Cal.com</title></Head>
<IframeResizer
autoResize
src={process.env.NEXT_PUBLIC_SWAGGER_DOCS_URL || "https://developer.cal.com"}
frameBorder="0"
style={{
width: useWindowSize().width > 768 ? "calc(100vw - 16rem)": "100vw",
minHeight: useWindowSize().width > 768 ? "100vh" : "200vh",
height: "auto",
border: 0,
}}
/>
</Bleed>

View File

@@ -27,7 +27,7 @@ or
1. Clone calendso-docker
```bash
git clone git@github.com:calendso/calendso-docker.git --recursive
git clone --recursive https://github.com/calendso/docker.git calendso-docker
```
2. Update `.env` if needed
@@ -40,7 +40,7 @@ or
4. Start prisma studio
```bash
docker-compose exec calendso -- npx prisma studio
docker-compose exec calendso npx prisma studio
```
5. Open a browser to [port 5555](http://localhost:5555) on your localhost to look at or modify the database content.

View File

@@ -1,5 +1,8 @@
{
"extends": "@calcom/tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
"compilerOptions": {
"noImplicitAny": false
}
}

View File

@@ -0,0 +1 @@
NEXT_PUBLIC_SWAGGER_DOCS_URL=http://localhost:3002/api/docs

34
apps/swagger/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel

34
apps/swagger/README.md Normal file
View File

@@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -0,0 +1,353 @@
import * as OpenAPISnippet from "openapi-snippet";
export const requestSnippets = {
generators: {
curl_bash: {
title: "cURL (bash)",
syntax: "bash",
},
curl_powershell: {
title: "cURL (PowerShell)",
syntax: "powershell",
},
curl_cmd: {
title: "cURL (CMD)",
syntax: "bash",
},
node: {
title: "Node",
syntax: "node",
},
},
defaultExpanded: true,
languages: ["node", "curl_bash"],
};
// Since swagger-ui-react was not configured to change the request snippets some workarounds required
// configuration will be added programatically
// Custom Plugin
export const SnippedGenerator = {
statePlugins: {
// extend some internals to gain information about current path, method and spec in the generator function metioned later
spec: {
wrapSelectors: {
requestFor: (ori, system) => (state, path, method) => {
return ori(path, method)
?.set("spec", state.get("json", {}))
?.setIn(["oasPathMethod", "path"], path)
?.setIn(["oasPathMethod", "method"], method);
},
mutatedRequestFor: (ori) => (state, path, method) => {
return ori(path, method)
?.set("spec", state.get("json", {}))
?.setIn(["oasPathMethod", "path"], path)
?.setIn(["oasPathMethod", "method"], method);
},
},
},
// extend the request snippets core plugin
requestSnippets: {
wrapSelectors: {
// add additional snippet generators here
getSnippetGenerators:
(ori, system) =>
(state, ...args) =>
ori(state, ...args)
// add node native snippet generator
// .set(
// // key
// "node_native",
// // config and generator function
// system.Im.fromJS({
// title: "NodeJs Native",
// syntax: "javascript",
// hostname: "test",
// fn: (req) => {
// // get extended info about request
// const { spec, oasPathMethod } = req.toJS();
// const { path, method } = oasPathMethod;
// // run OpenAPISnippet for target node
// const targets = ["node_native"];
// let snippet;
// try {
// // set request snippet content
// snippet = OpenAPISnippet.getEndpointSnippets(
// spec,
// path,
// method,
// targets
// // Since I don't know why hostname was undefinedundefined, I harcoded it here
// ).snippets[0].content;
// } catch (err) {
// // set to error in case it happens the npm package has some flaws
// snippet = JSON.stringify(snippet);
// }
// // return stringified snipped
// return snippet;
// },
// })
// )
.set(
// key
"node_fetch",
// config and generator function
system.Im.fromJS({
title: "NodeJS",
syntax: "javascript",
fn: (req) => {
// get extended info about request
const { spec, oasPathMethod } = req.toJS();
const { path, method } = oasPathMethod;
// run OpenAPISnippet for target node
const targets = ["node_fetch"];
let snippet;
try {
// set request snippet content
snippet = OpenAPISnippet.getEndpointSnippets(spec, path, method, targets).snippets[0]
.content;
} catch (err) {
// set to error in case it happens the npm package has some flaws
snippet = JSON.stringify(snippet);
}
// return stringified snipped
return snippet;
},
})
)
.set(
// key
"shell_httpie",
// config and generator function
system.Im.fromJS({
title: "HTTPie",
syntax: "bash",
fn: (req) => {
// get extended info about request
const { spec, oasPathMethod } = req.toJS();
const { path, method } = oasPathMethod;
// run OpenAPISnippet for target node
const targets = ["shell_httpie"];
let snippet;
try {
// set request snippet content
snippet = OpenAPISnippet.getEndpointSnippets(spec, path, method, targets).snippets[0]
.content;
} catch (err) {
// set to error in case it happens the npm package has some flaws
snippet = JSON.stringify(snippet);
}
// return stringified snipped
return snippet;
},
})
)
.set(
// key
"php_curl",
// config and generator function
system.Im.fromJS({
title: "PHP",
syntax: "php",
fn: (req) => {
// get extended info about request
const { spec, oasPathMethod } = req.toJS();
const { path, method } = oasPathMethod;
// run OpenAPISnippet for target node
const targets = ["php_curl"];
let snippet;
try {
// set request snippet content
snippet = OpenAPISnippet.getEndpointSnippets(spec, path, method, targets).snippets[0]
.content;
} catch (err) {
// set to error in case it happens the npm package has some flaws
snippet = JSON.stringify(snippet);
}
// return stringified snipped
return snippet;
},
})
)
.set(
// key
"java_okhttp",
// config and generator function
system.Im.fromJS({
title: "Java",
syntax: "java",
fn: (req) => {
// get extended info about request
const { spec, oasPathMethod } = req.toJS();
const { path, method } = oasPathMethod;
console.log(spec, oasPathMethod, path, method);
// run OpenAPISnippet for target node
const targets = ["java_okhttp"];
let snippet;
try {
// set request snippet content
snippet = OpenAPISnippet.getEndpointSnippets(spec, path, method, targets).snippets[0]
.content;
} catch (err) {
// set to error in case it happens the npm package has some flaws
snippet = JSON.stringify(snippet);
}
// return stringified snipped
return snippet;
},
})
)
// .set(
// // key
// "java",
// // config and generator function
// system.Im.fromJS({
// title: "Java (Unirest)",
// syntax: "java",
// fn: (req) => {
// // get extended info about request
// const { spec, oasPathMethod } = req.toJS();
// const { path, method } = oasPathMethod;
// // run OpenAPISnippet for target node
// const targets = ["java"];
// let snippet;
// try {
// // set request snippet content
// snippet = OpenAPISnippet.getEndpointSnippets(
// spec,
// path,
// method,
// targets
// ).snippets[0].content;
// } catch (err) {
// // set to error in case it happens the npm package has some flaws
// snippet = JSON.stringify(snippet);
// }
// // return stringified snipped
// return snippet;
// },
// })
// )
// .set(
// // key
// "c_libcurl",
// // config and generator function
// system.Im.fromJS({
// title: "C (libcurl) ",
// syntax: "bash",
// fn: (req) => {
// // get extended info about request
// const { spec, oasPathMethod } = req.toJS();
// const { path, method } = oasPathMethod;
// // run OpenAPISnippet for target node
// const targets = ["c_libcurl"];
// let snippet;
// try {
// // set request snippet content
// snippet = OpenAPISnippet.getEndpointSnippets(
// spec,
// path,
// method,
// targets
// ).snippets[0].content;
// } catch (err) {
// // set to error in case it happens the npm package has some flaws
// snippet = JSON.stringify(snippet);
// }
// // return stringified snipped
// return snippet;
// },
// })
// )
.set(
// key
"go_native",
// config and generator function
system.Im.fromJS({
title: "Go",
syntax: "bash",
fn: (req) => {
// get extended info about request
const { spec, oasPathMethod } = req.toJS();
const { path, method } = oasPathMethod;
// run OpenAPISnippet for target node
const targets = ["go_native"];
let snippet;
try {
// set request snippet content
snippet = OpenAPISnippet.getEndpointSnippets(spec, path, method, targets).snippets[0]
.content;
} catch (err) {
// set to error in case it happens the npm package has some flaws
snippet = JSON.stringify(snippet);
}
// return stringified snipped
return snippet;
},
})
)
.set(
// key
"ruby",
// config and generator function
system.Im.fromJS({
title: "Ruby",
syntax: "ruby",
fn: (req) => {
// get extended info about request
const { spec, oasPathMethod } = req.toJS();
const { path, method } = oasPathMethod;
// run OpenAPISnippet for target node
const targets = ["ruby"];
let snippet;
try {
// set request snippet content
snippet = OpenAPISnippet.getEndpointSnippets(spec, path, method, targets).snippets[0]
.content;
} catch (err) {
// set to error in case it happens the npm package has some flaws
snippet = JSON.stringify(snippet);
}
// return stringified snipped
return snippet;
},
})
)
.set(
// key
"python",
// config and generator function
system.Im.fromJS({
title: "Python",
syntax: "python",
fn: (req) => {
// get extended info about request
const { spec, oasPathMethod } = req.toJS();
const { path, method } = oasPathMethod;
// run OpenAPISnippet for target node
const targets = ["python"];
let snippet;
try {
// set request snippet content
snippet = OpenAPISnippet.getEndpointSnippets(spec, path, method, targets).snippets[0]
.content;
} catch (err) {
// set to error in case it happens the npm package has some flaws
snippet = JSON.stringify(snippet);
}
// return stringified snipped
return snippet;
},
})
),
},
},
},
};

5
apps/swagger/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

25
apps/swagger/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "@calcom/swagger",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "PORT=4200 next dev",
"build": "next build",
"start": "PORT=4200 next start"
},
"dependencies": {
"highlight.js": "^11.5.1",
"isarray": "2.0.5",
"next": "12.1.5",
"openapi-snippet": "^0.13.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"swagger-ui-react": "4.10.3"
},
"devDependencies": {
"@types/node": "17.0.27",
"@types/react": "17.0.43",
"@types/react-dom": "17.0.14",
"typescript": "4.6.3"
}
}

View File

@@ -0,0 +1,10 @@
import "highlight.js/styles/default.css";
import "swagger-ui-react/swagger-ui.css";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;

View File

@@ -0,0 +1,26 @@
import dynamic from "next/dynamic";
import { SwaggerUI } from "swagger-ui-react";
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
const SwaggerUIDynamic: SwaggerUI & { url: string } = dynamic(() => import("swagger-ui-react"), {
ssr: false,
});
export default function APIDocs() {
return (
<SwaggerUIDynamic
url={process.env.NEXT_PUBLIC_SWAGGER_DOCS_URL || "https://api.cal.com/docs"}
persistAuthorization={true}
supportedSubmitMethods={["get", "post", "delete", "put", "options", "patch"]}
requestSnippetsEnabled={true}
requestSnippets={requestSnippets}
plugins={[SnippedGenerator]}
tryItOutEnabled={true}
syntaxHighlight={true}
enableCORS={false} // Doesn't seem to work either
docExpansion="list"
filter={true}
/>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,102 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
@media (max-width: 768px) {
.swagger-ui .opblock-tag {
font-size: 90% !important;
}
.swagger-ui .opblock .opblock-summary {
display: grid;
flex-direction: column;
}
.opblock-summary-path {
flex-shrink: 0;
max-width: 100% !important;
padding: 10px 5px !important;
}
.opblock-summary-description {
font-size: 16px !important;
padding: 0px 5px;
}
.swagger-ui .scheme-container .schemes {
align-items: center;
display: flex;
flex-direction: column;
}
.swagger-ui .info .title {
color: #3b4151;
font-family: sans-serif;
font-size: 22px;
}
.swagger-ui .scheme-container {
padding: 14px 0;
}
.swagger-ui .info {
margin: 10px 0;
}
.swagger-ui .auth-wrapper {
margin: 10px 0;
}
.swagger-ui .authorization__btn {
display: none;
}
.swagger-ui .opblock {
margin: 0 0 5px;
}
button.opblock-summary-control > svg {
display: none;
}
.swagger-ui .filter .operation-filter-input {
border: 2px solid #d8dde7;
margin: 5px 5px;
padding: 5px;
width: 100vw;
}
.swagger-ui .wrapper {
padding: 0 4px;
width: 100%;
}
.swagger-ui .info .title small {
top: 5px;
}
.swagger-ui a.nostyle, .swagger-ui a.nostyle:visited {
width: 100%;
}
div.request-snippets > div.curl-command > div:nth-child(1) {
overscroll-behavior: contain;
overflow-x: scroll;
}
.swagger-ui .opblock-body pre.microlight {
font-size: 9px;
}
.swagger-ui table tbody tr td {
padding: 0px 0 0;
vertical-align: none;
}
td.response-col_description > div > div > p {
font-size: 12px;
}
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > tbody > tr {
display: flex;
width: 100vw;
flex-direction: column;
font-size: 60%;
}
div.no-margin > div > div.responses-wrapper > div.responses-inner > div > div > table > thead > tr {
display: none;
}
}

View File

@@ -0,0 +1,24 @@
{
"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"],
"exclude": ["node_modules"]
}

2583
apps/swagger/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,104 +0,0 @@
# Set this value to 'agree' to accept our license:
# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
#
# Summary of terms:
# - The codebase has to stay open source, whether it was modified or not
# - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
NEXT_PUBLIC_LICENSE_CONSENT=''
# ⚠️ ⚠️ ⚠️ DATABASE_URL got moved to `packages/prisma/.env.example` ⚠️ ⚠️ ⚠️
# Needed to enable Google Calendar integration and Login with Google
# @see https://github.com/calendso/calendso#obtaining-the-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`
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
GOOGLE_LOGIN_ENABLED=false
BASE_URL='http://localhost:3000'
NEXT_PUBLIC_APP_URL='http://localhost:3000'
JWT_SECRET='secret'
# This is used so we can bypass emails in auth flows for E2E testing
PLAYWRIGHT_SECRET=
# To enable SAML login, set both these variables
# @see https://github.com/calendso/calendso/tree/main/ee#setting-up-saml-login
# SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml"
# SAML_ADMINS='pro@example.com'
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
##PGSSLMODE='no-verify'
# @see: https://github.com/calendso/calendso/issues/263
# Required for Vercel hosting - set NEXTAUTH_URL to equal your BASE_URL
# NEXTAUTH_URL='http://localhost:3000'
# Remove this var if you don't want Cal to collect anonymous usage
NEXT_PUBLIC_TELEMETRY_KEY=js.2pvs2bbpqq1zxna97wcml.oi2jzirnbj1ev4tc57c5r
# Used for the Office 365 / Outlook.com Calendar integration
MS_GRAPH_CLIENT_ID=
MS_GRAPH_CLIENT_SECRET=
# Used for the Zoom integration
ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET=
#Used for the Daily integration
DAILY_API_KEY=
DAILY_SCALE_PLAN=''
# Used for the Tandem integration -- contact support@tandem.chat to for API access.
TANDEM_CLIENT_ID=""
TANDEM_CLIENT_SECRET=""
TANDEM_BASE_URL="https://tandem.chat"
# E-mail settings
# Cal uses nodemailer (@see https://nodemailer.com/about/) to provide email sending. As such we are trying to
# allow access to the nodemailer transports from the .env file. E-mail templates are accessible within lib/emails/
# Configures the global From: header whilst sending emails.
EMAIL_FROM='notifications@yourselfhostedcal.com'
# Configure SMTP settings (@see https://nodemailer.com/smtp/).
# Note: The below configuration for Office 365 has been verified to work.
EMAIL_SERVER_HOST='smtp.office365.com'
EMAIL_SERVER_PORT=587
EMAIL_SERVER_USER='<office365_emailAddress>'
# Keep in mind that if you have 2FA enabled, you will need to provision an App Password.
EMAIL_SERVER_PASSWORD='<office365_password>'
# The following configuration for Gmail has been verified to work.
# EMAIL_SERVER_HOST='smtp.gmail.com'
# EMAIL_SERVER_PORT=465
# EMAIL_SERVER_USER='<gmail_emailAddress>'
## You will need to provision an App Password.
## @see https://support.google.com/accounts/answer/185833
# EMAIL_SERVER_PASSWORD='<gmail_app_password>'
# ApiKey for cronjobs
CRON_API_KEY='0cc0e6c35519bba620c9360cfe3e68d0'
# Stripe Config
NEXT_PUBLIC_STRIPE_PUBLIC_KEY= # pk_test_...
STRIPE_PRIVATE_KEY= # sk_test_...
STRIPE_CLIENT_ID= # ca_...
STRIPE_WEBHOOK_SECRET= # whsec_...
PAYMENT_FEE_PERCENTAGE=0.005 # Take 0.5% commission
PAYMENT_FEE_FIXED=10 # Take 10 additional cents commission
# Application Key for symmetric encryption and decryption
# must be 32 bytes for AES256 encryption algorithm
CALENDSO_ENCRYPTION_KEY=
# Intercom Config
NEXT_PUBLIC_INTERCOM_APP_ID=
# Zendesk Config
NEXT_PUBLIC_ZENDESK_KEY=

3
apps/web/.gitignore vendored
View File

@@ -61,3 +61,6 @@ yarn-error.log*
# Typescript
tsconfig.tsbuildinfo
# Autogenerated embed content
public/embed

241
apps/web/components/App.tsx Normal file
View File

@@ -0,0 +1,241 @@
import {
BookOpenIcon,
DocumentTextIcon,
ExternalLinkIcon,
FlagIcon,
MailIcon,
ShieldCheckIcon,
} from "@heroicons/react/outline";
import { ChevronLeftIcon } from "@heroicons/react/solid";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { App as AppType } from "@calcom/types/App";
import { Button } from "@calcom/ui";
import Shell from "@components/Shell";
import Badge from "@components/ui/Badge";
export default function App({
name,
type,
logo,
body,
categories,
author,
price = 0,
commission,
isGlobal = false,
feeType,
docs,
website,
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;
}) {
const { t } = useLocale();
const priceInDollar = Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
useGrouping: false,
}).format(price);
const [installedApp, setInstalledApp] = useState(false);
useEffect(() => {
async function getInstalledApp(appCredentialType: string) {
const queryParam = new URLSearchParams();
queryParam.set("app-credential-type", appCredentialType);
try {
const result = await fetch(`/api/app-store/installed?${queryParam.toString()}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (result.status === 200) {
setInstalledApp(true);
}
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
}
}
}
getInstalledApp(type);
}, []);
return (
<>
<Shell large>
<div className="-mx-4 md:-mx-8">
<div className="bg-gray-50 px-4">
<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" 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">
{isGlobal || installedApp ? (
<Button color="secondary" disabled title="This app is globally installed">
{t("installed")}
</Button>
) : (
<InstallAppButton
type={type}
render={(buttonProps) => (
<Button data-testid="install-app-button" {...buttonProps}>
{t("install_app")}
</Button>
)}
/>
)}
{price !== 0 && (
<small className="block text-right">
{feeType === "usage-based"
? commission + "% + " + priceInDollar + "/booking"
: priceInDollar}
{feeType === "monthly" && "/" + t("month")}
</small>
)}
</div>
</div>
{/* reintroduce once we show permissions and features
<NavTabs tabs={tabs} linkProps={{ shallow: true }} /> */}
</div>
<div className="justify-between px-4 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>
</Shell>
</>
);
}

View File

@@ -0,0 +1,30 @@
import { useSession } from "next-auth/react";
import React from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import NavTabs from "./NavTabs";
export default function AppsShell({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
const { status } = useSession();
const tabs = [
{
name: t("app_store"),
href: "/apps",
},
{
name: t("installed_apps"),
href: "/apps/installed",
},
];
return (
<>
<div className="mb-12 block lg:hidden">
{status === "authenticated" && <NavTabs tabs={tabs} linkProps={{ shallow: true }} />}
</div>
<main className="pb-6">{children}</main>
</>
);
}

View File

@@ -11,6 +11,10 @@ export default function BookingsShell({ children }: { children: React.ReactNode
name: t("upcoming"),
href: "/bookings/upcoming",
},
{
name: t("recurring"),
href: "/bookings/recurring",
},
{
name: t("past"),
href: "/bookings/past",

View File

@@ -1,5 +1,7 @@
import { useEffect } from "react";
import { useBrandColors } from "@calcom/embed-core";
const brandColor = "#292929";
const brandTextColor = "#ffffff";
const darkBrandColor = "#fafafa";
@@ -220,6 +222,8 @@ const BrandColor = ({
lightVal: string | undefined | null;
darkVal: string | undefined | null;
}) => {
const embedBrandingColors = useBrandColors();
lightVal = embedBrandingColors.brandColor || lightVal;
// convert to 6 digit equivalent if 3 digit code is entered
lightVal = normalizeHexCode(lightVal, false);
darkVal = normalizeHexCode(darkVal, true);
@@ -235,6 +239,34 @@ const BrandColor = ({
: "#" + 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);

View File

@@ -1,11 +1,11 @@
import React, { useEffect, useState } from "react";
import Select from "react-select";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
import Button from "@components/ui/Button";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;
isLoading?: boolean;
@@ -25,20 +25,18 @@ const DestinationCalendarSelector = ({
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
useEffect(() => {
if (!selectedOption) {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
.flat()
.find((cal) => cal.externalId === value);
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: selected.name || "",
});
}
if (selected) {
setSelectedOption({
value: `${selected.integration}:${selected.externalId}`,
label: selected.name || "",
});
}
}, [query.data?.connectedCalendars, selectedOption, value]);
}, [query.data?.connectedCalendars, value]);
if (!query.data?.connectedCalendars.length) {
return null;
@@ -53,11 +51,14 @@ const DestinationCalendarSelector = ({
})),
})) ?? [];
return (
<div className="relative">
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
{/* There's no easy way to customize the displayed value for a Select, so we fake it. */}
{!hidePlaceholder && (
<div className="pointer-events-none absolute z-10">
<Button size="sm" color="secondary" className="m-[1px] rounded-sm border-transparent">
<div className="pointer-events-none absolute z-10 w-full">
<Button
size="sm"
color="secondary"
className="m-[1px] w-[calc(100%_-_40px)] overflow-hidden overflow-ellipsis whitespace-nowrap rounded-sm border-none leading-5">
{t("select_destination_calendar")}: {selectedOption?.label || ""}
</Button>
</div>
@@ -67,7 +68,7 @@ const DestinationCalendarSelector = ({
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
options={options}
isSearchable={false}
className="focus:border-primary-500 focus:ring-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
onChange={(option) => {
setSelectedOption(option);
if (!option) {

View File

@@ -0,0 +1,900 @@
import { CodeIcon, EyeIcon, SunIcon, ChevronRightIcon, ArrowLeftIcon } from "@heroicons/react/solid";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import { useRouter } from "next/router";
import { useRef, useState } from "react";
import { components, ControlProps, SingleValue } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { EventType } from "@calcom/prisma/client";
import { Button, Switch } from "@calcom/ui";
import { Dialog, DialogContent, DialogClose } from "@calcom/ui/Dialog";
import { InputLeading, Label, TextArea, TextField } from "@calcom/ui/form/fields";
import { WEBAPP_URL, EMBED_LIB_URL } from "@lib/config/constants";
import { trpc } from "@lib/trpc";
import NavTabs from "@components/NavTabs";
import ColorPicker from "@components/ui/colorpicker";
import Select from "@components/ui/form/Select";
type EmbedType = "inline" | "floating-popup" | "element-click";
const queryParamsForDialog = ["embedType", "tabName", "eventTypeId"];
const embeds: {
illustration: React.ReactElement;
title: string;
subtitle: string;
type: EmbedType;
}[] = [
{
title: "Inline Embed",
subtitle: "Loads your Cal scheduling page directly inline with your other website content",
type: "inline",
illustration: (
<svg
width="100%"
height="100%"
className="rounded-md"
viewBox="0 0 308 265"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="2" fill="#E1E1E1" />
<rect x="48" y="86.5" width="48" height="4" rx="1" fill="#E1E1E1" />
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="125.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<path
d="M61 124.5H67V122.5H61V124.5ZM68 125.5V131.5H70V125.5H68ZM67 132.5H61V134.5H67V132.5ZM60 131.5V125.5H58V131.5H60ZM61 132.5C60.4477 132.5 60 132.052 60 131.5H58C58 133.157 59.3431 134.5 61 134.5V132.5ZM68 131.5C68 132.052 67.5523 132.5 67 132.5V134.5C68.6569 134.5 70 133.157 70 131.5H68ZM67 124.5C67.5523 124.5 68 124.948 68 125.5H70C70 123.843 68.6569 122.5 67 122.5V124.5ZM61 122.5C59.3431 122.5 58 123.843 58 125.5H60C60 124.948 60.4477 124.5 61 124.5V122.5Z"
fill="#3E3E3E"
/>
<rect x="73" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="73" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="121" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="73" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="121" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="73" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="24.5" y="51" width="139" height="163" rx="1.5" stroke="#292929" />
<rect x="176" y="50.5" width="108" height="164" rx="2" fill="#E1E1E1" />
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
{/* <path
d="M2 1H306V-1H2V1ZM307 2V263H309V2H307ZM306 264H2V266H306V264ZM1 263V1.99999H-1V263H1ZM2 264C1.44772 264 1 263.552 1 263H-1C-1 264.657 0.343147 266 2 266V264ZM307 263C307 263.552 306.552 264 306 264V266C307.657 266 309 264.657 309 263H307ZM306 1C306.552 1 307 1.44772 307 2H309C309 0.343145 307.657 -1 306 -1V1ZM2 -1C0.343151 -1 -1 0.343133 -1 1.99999H1C1 1.44771 1.44771 1 2 1V-1Z"
fill="#CFCFCF"
/> */}
</svg>
),
},
{
title: "Floating pop-up button",
subtitle: "Adds a floating button on your site that launches Cal in a dialog.",
type: "floating-popup",
illustration: (
<svg
width="100%"
height="100%"
className="rounded-md"
viewBox="0 0 308 265"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="226" y="223.5" width="66" height="26" rx="2" fill="#292929" />
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
{/* <path
d="M2 1H306V-1H2V1ZM307 2V263H309V2H307ZM306 264H2V266H306V264ZM1 263V1.99999H-1V263H1ZM2 264C1.44772 264 1 263.552 1 263H-1C-1 264.657 0.343147 266 2 266V264ZM307 263C307 263.552 306.552 264 306 264V266C307.657 266 309 264.657 309 263H307ZM306 1C306.552 1 307 1.44772 307 2H309C309 0.343145 307.657 -1 306 -1V1ZM2 -1C0.343151 -1 -1 0.343133 -1 1.99999H1C1 1.44771 1.44771 1 2 1V-1Z"
fill="#CFCFCF"
/> */}
</svg>
),
},
{
title: "Pop up via element click",
subtitle: "Open your Cal dialog when someone clicks an element.",
type: "element-click",
illustration: (
<svg
width="100%"
height="100%"
className="rounded-md"
viewBox="0 0 308 265"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" fill="#F8F8F8" />
<rect opacity="0.8" x="108" y="85" width="80" height="8" rx="2" fill="#E1E1E1" />
<rect x="108" y="97" width="48" height="4" rx="1" fill="#E1E1E1" />
<rect x="109" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="110" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="133" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="169" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="181" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="169" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="181" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="136" width="6" height="6" rx="1" fill="#3E3E3E" />
<path
d="M121 135H127V133H121V135ZM128 136V142H130V136H128ZM127 143H121V145H127V143ZM120 142V136H118V142H120ZM121 143C120.448 143 120 142.552 120 142H118C118 143.657 119.343 145 121 145V143ZM128 142C128 142.552 127.552 143 127 143V145C128.657 145 130 143.657 130 142H128ZM127 135C127.552 135 128 135.448 128 136H130C130 134.343 128.657 133 127 133V135ZM121 133C119.343 133 118 134.343 118 136H120C120 135.448 120.448 135 121 135V133Z"
fill="#3E3E3E"
/>
<rect x="133" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="169" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="181" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="169" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="181" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="169" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="181" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="193" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="145" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="157" y="172" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="169" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" stroke="#292929" />
{/* <path
d="M2 1H306V-1H2V1ZM307 2V263H309V2H307ZM306 264H2V266H306V264ZM1 263V1.99999H-1V263H1ZM2 264C1.44772 264 1 263.552 1 263H-1C-1 264.657 0.343147 266 2 266V264ZM307 263C307 263.552 306.552 264 306 264V266C307.657 266 309 264.657 309 263H307ZM306 1C306.552 1 307 1.44772 307 2H309C309 0.343145 307.657 -1 306 -1V1ZM2 -1C0.343151 -1 -1 0.343133 -1 1.99999H1C1 1.44771 1.44771 1 2 1V-1Z"
fill="#CFCFCF"
/> */}
</svg>
),
},
];
function getEmbedSnippetString() {
// TODO: Import this string from @calcom/embed-snippet
return `
(function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar); return; } p(cal, ar); }; })(window, "${EMBED_LIB_URL}", "init");
Cal("init", {origin:"${WEBAPP_URL}"});
`;
}
const EmbedNavBar = () => {
const { t } = useLocale();
const tabs = [
{
name: t("Embed"),
tabName: "embed-code",
icon: CodeIcon,
},
{
name: t("Preview"),
tabName: "embed-preview",
icon: EyeIcon,
},
];
return <NavTabs data-testid="embed-tabs" tabs={tabs} linkProps={{ shallow: true }} />;
};
const ThemeSelectControl = ({ children, ...props }: ControlProps<any, false>) => {
return (
<components.Control {...props}>
<SunIcon className="h-[32px] w-[32px] text-gray-500" />
{children}
</components.Control>
);
};
const ChooseEmbedTypesDialogContent = () => {
const { t } = useLocale();
const router = useRouter();
return (
<DialogContent size="lg">
<div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{t("how_you_want_add_cal_site")}
</h3>
<div>
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site")}</p>
</div>
</div>
<div className="flex">
{embeds.map((embed, index) => (
<button
className="mr-2 w-1/3 p-3 text-left hover:rounded-md hover:border hover:bg-neutral-100"
key={index}
data-testid={embed.type}
onClick={() => {
router.push({
query: {
...router.query,
embedType: embed.type,
},
});
}}>
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
{embed.illustration}
</div>
<div className="mt-2 font-medium text-neutral-900">{embed.title}</div>
<p className="text-sm text-gray-500">{embed.subtitle}</p>
</button>
))}
</div>
</DialogContent>
);
};
const EmbedTypeCodeAndPreviewDialogContent = ({
eventTypeId,
embedType,
}: {
eventTypeId: EventType["id"];
embedType: EmbedType;
}) => {
const { t } = useLocale();
const router = useRouter();
const iframeRef = useRef<HTMLIFrameElement>(null);
const embedCode = useRef<HTMLTextAreaElement>(null);
const embed = embeds.find((embed) => embed.type === embedType);
const { data: eventType, isLoading } = trpc.useQuery([
"viewer.eventTypes.get",
{
id: +eventTypeId,
},
]);
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
const [previewState, setPreviewState] = useState({
inline: {
width: "100%",
height: "100%",
},
theme: "auto",
floatingPopup: {},
elementClick: {},
palette: {
brandColor: "#000000",
},
});
const close = () => {
const noPopupQuery = {
...router.query,
};
delete noPopupQuery.dialog;
queryParamsForDialog.forEach((queryParam) => {
delete noPopupQuery[queryParam];
});
router.push({
query: noPopupQuery,
});
};
// Use embed-code as default tab
if (!router.query.tabName) {
router.query.tabName = "embed-code";
router.push({
query: {
...router.query,
},
});
}
if (isLoading) {
return null;
}
if (!embed || !eventType) {
close();
return null;
}
const calLink = `${eventType.team ? `team/${eventType.team.slug}` : eventType.users[0].username}/${
eventType.slug
}`;
// TODO: Not sure how to make these template strings look better formatted.
// This exact formatting is required to make the code look nicely formatted together.
const getEmbedUIInstructionString = () =>
`Cal("ui", {
${getThemeForSnippet() ? 'theme: "' + previewState.theme + '",\n ' : ""}styles: {
branding: ${JSON.stringify(previewState.palette)}
}
})`;
const getEmbedTypeSpecificString = () => {
if (embedType === "inline") {
return `
Cal("inline", {
elementOrSelector:"#my-cal-inline",
calLink: "${calLink}"
});
${getEmbedUIInstructionString().trim()}`;
} else if (embedType === "floating-popup") {
let floatingButtonArg = {
calLink,
...previewState.floatingPopup,
};
return `
Cal("floatingButton", ${JSON.stringify(floatingButtonArg)});
${getEmbedUIInstructionString().trim()}`;
} else if (embedType === "element-click") {
return `//Important: Also, add data-cal-link="${calLink}" attribute to the element you want to open Cal on click
${getEmbedUIInstructionString().trim()}`;
}
return "";
};
const getThemeForSnippet = () => {
return previewState.theme !== "auto" ? previewState.theme : null;
};
const getDimension = (dimension: string) => {
if (dimension.match(/^\d+$/)) {
dimension = `${dimension}%`;
}
return dimension;
};
const addToPalette = (update: typeof previewState["palette"]) => {
setPreviewState((previewState) => {
return {
...previewState,
palette: {
...previewState.palette,
...update,
},
};
});
};
const previewInstruction = (instruction: { name: string; arg: any }) => {
iframeRef.current?.contentWindow?.postMessage(
{
mode: "cal:preview",
type: "instruction",
instruction,
},
"*"
);
};
const inlineEmbedDimensionUpdate = ({ width, height }: { width: string; height: string }) => {
iframeRef.current?.contentWindow?.postMessage(
{
mode: "cal:preview",
type: "inlineEmbedDimensionUpdate",
data: {
width: getDimension(width),
height: getDimension(height),
},
},
"*"
);
};
previewInstruction({
name: "ui",
arg: {
theme: previewState.theme,
styles: {
branding: {
...previewState.palette,
},
},
},
});
if (embedType === "floating-popup") {
previewInstruction({
name: "floatingButton",
arg: {
attributes: {
id: "my-floating-button",
},
...previewState.floatingPopup,
},
});
}
if (embedType === "inline") {
inlineEmbedDimensionUpdate({
width: previewState.inline.width,
height: previewState.inline.height,
});
}
const ThemeOptions = [
{ value: "auto", label: "Auto Theme" },
{ value: "dark", label: "Dark Theme" },
{ value: "light", label: "Light Theme" },
];
const FloatingPopupPositionOptions = [
{
value: "bottom-right",
label: "Bottom Right",
},
{
value: "bottom-left",
label: "Bottom Left",
},
];
return (
<DialogContent size="xl">
<div className="flex">
<div className="flex w-1/3 flex-col bg-white p-6">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
<button
onClick={() => {
const newQuery = { ...router.query };
delete newQuery.embedType;
delete newQuery.tabName;
router.push({
query: {
...newQuery,
},
});
}}>
<ArrowLeftIcon className="mr-4 w-4"></ArrowLeftIcon>
</button>
{embed.title}
</h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")}></hr>
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
open={isEmbedCustomizationOpen}
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger
type="button"
className="flex w-full items-center text-base font-medium text-neutral-900">
<div>
{embedType === "inline"
? "Inline Embed Customization"
: embedType === "floating-popup"
? "Floating Popup Customization"
: "Element Click Customization"}
</div>
<ChevronRightIcon
className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent className="text-sm">
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-sm">Embed Window Sizing</div>
<div className="justify-left flex items-center">
<TextField
name="width"
labelProps={{ className: "hidden" }}
required
value={previewState.inline.width}
onChange={(e) => {
setPreviewState((previewState) => {
let width = e.target.value || "100%";
return {
...previewState,
inline: {
...previewState.inline,
width,
},
};
});
}}
addOnLeading={<InputLeading>W</InputLeading>}
/>
<span className="p-2">x</span>
<TextField
labelProps={{ className: "hidden" }}
name="height"
value={previewState.inline.height}
required
onChange={(e) => {
const height = e.target.value || "100%";
setPreviewState((previewState) => {
return {
...previewState,
inline: {
...previewState.inline,
height,
},
};
});
}}
addOnLeading={<InputLeading>H</InputLeading>}
/>
</div>
</div>
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
)}>
<div className="text-sm">Button Text</div>
{/* Default Values should come from preview iframe */}
<TextField
name="buttonText"
labelProps={{ className: "hidden" }}
onChange={(e) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonText: e.target.value,
},
};
});
}}
defaultValue="Book my Cal"
required
/>
</div>
<div
className={classNames(
"mt-4 flex items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
)}>
<div className="text-sm">Display Calendar Icon Button</div>
<Switch
defaultChecked={true}
onCheckedChange={(checked) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
hideButtonIcon: !checked,
},
};
});
}}></Switch>
</div>
<div
className={classNames(
"mt-4 flex items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
)}>
<div>Position of Button</div>
<Select
onChange={(position) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonPosition: position?.value,
},
};
});
}}
defaultValue={FloatingPopupPositionOptions[0]}
options={FloatingPopupPositionOptions}></Select>
</div>
<div
className={classNames(
"mt-4 flex items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
)}>
<div>Button Color</div>
<div className="w-36">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonColor: color,
},
};
});
}}></ColorPicker>
</div>
</div>
<div
className={classNames(
"mt-4 flex items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
)}>
<div>Text Color</div>
<div className="w-36">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonTextColor: color,
},
};
});
}}></ColorPicker>
</div>
</div>
{/* <div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "flex" : "hidden"
)}>
<div>Button Color on Hover</div>
<div className="w-36">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
addToPalette({
"floating-popup-button-color-hover": color,
});
}}></ColorPicker>
</div>
</div> */}
</CollapsibleContent>
</Collapsible>
</div>
<hr className="mt-4"></hr>
<div className="mt-4 font-medium">
<Collapsible
open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div>
<ChevronRightIcon
className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`}
/>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-6 text-sm">
<Label className="flex items-center justify-between">
<div>Theme</div>
<Select
className="w-36"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
}}
onChange={(option) => {
if (!option) {
return;
}
setPreviewState((previewState) => {
return {
...previewState,
theme: option.value,
};
});
}}
options={ThemeOptions}></Select>
</Label>
{[
{ name: "brandColor", title: "Brand Color" },
// { name: "lightColor", title: "Light Color" },
// { name: "lighterColor", title: "Lighter Color" },
// { name: "lightestColor", title: "Lightest Color" },
// { name: "highlightColor", title: "Highlight Color" },
// { name: "medianColor", title: "Median Color" },
].map((palette) => (
<Label key={palette.name} className="flex items-center justify-between">
<div>{palette.title}</div>
<div className="w-36">
<ColorPicker
defaultValue="#000000"
onChange={(color) => {
//@ts-ignore - How to support dynamic palette names?
addToPalette({
[palette.name]: color,
});
}}></ColorPicker>
</div>
</Label>
))}
</div>
</CollapsibleContent>
</Collapsible>
</div>
</div>
<div className="w-2/3 bg-gray-50 p-6">
<EmbedNavBar />
<div>
<div
className={classNames(router.query.tabName === "embed-code" ? "block" : "hidden", "h-[75vh]")}>
<small className="flex py-4 text-neutral-500">{t("place_where_cal_widget_appear")}</small>
<TextArea
data-testid="embed-code"
ref={embedCode}
name="embed-code"
className="h-[36rem]"
readOnly
value={
`<!-- Cal ${embedType} embed code begins -->\n` +
(embedType === "inline"
? `<div style="width:${getDimension(previewState.inline.width)};height:${getDimension(
previewState.inline.height
)};overflow:scroll" id="my-cal-inline"></div>\n`
: "") +
`<script type="text/javascript">
${getEmbedSnippetString().trim()}
${getEmbedTypeSpecificString().trim()}
</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."
)}
</p>
</div>
<div className={router.query.tabName == "embed-preview" ? "block" : "hidden"}>
<iframe
ref={iframeRef}
data-testid="embed-preview"
className="border-1 h-[75vh] border"
width="100%"
height="100%"
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
/>
</div>
</div>
<div className="mt-8 flex flex-row-reverse gap-x-2">
<Button
type="submit"
onClick={() => {
if (!embedCode.current) {
return;
}
navigator.clipboard.writeText(embedCode.current.value);
showToast(t("code_copied"), "success");
}}>
{t("copy_code")}
</Button>
<DialogClose asChild>
<Button color="secondary">{t("Close")}</Button>
</DialogClose>
</div>
</div>
</div>
</DialogContent>
);
};
export const EmbedDialog = () => {
const router = useRouter();
const eventTypeId: EventType["id"] = +(router.query.eventTypeId as string);
return (
<Dialog name="embed" clearQueryParamsOnClose={queryParamsForDialog}>
{!router.query.embedType ? (
<ChooseEmbedTypesDialogContent />
) : (
<EmbedTypeCodeAndPreviewDialogContent
eventTypeId={eventTypeId}
embedType={router.query.embedType as EmbedType}
/>
)}
</Dialog>
);
};
export const EmbedButton = ({
eventTypeId,
className = "",
dark,
...props
}: {
eventTypeId: EventType["id"];
className: string;
dark?: boolean;
}) => {
const { t } = useLocale();
const router = useRouter();
className = classNames(className, "hidden lg:flex");
const openEmbedModal = () => {
const query = {
...router.query,
dialog: "embed",
eventTypeId,
};
router.push(
{
pathname: router.pathname,
query,
},
undefined,
{ shallow: true }
);
};
return (
<Button
type="button"
color="minimal"
size="sm"
className={className}
{...props}
data-test-eventtype-id={eventTypeId}
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>
{t("Embed")}
</Button>
);
};

View File

@@ -1,13 +1,14 @@
import { FormEvent, useCallback, useEffect, useState } from "react";
import Cropper from "react-easy-crop";
import Button from "@calcom/ui/Button";
import { DialogClose, DialogTrigger, Dialog, DialogContent } from "@calcom/ui/Dialog";
import { Area, getCroppedImg } from "@lib/cropImage";
import { useFileReader } from "@lib/hooks/useFileReader";
import { useLocale } from "@lib/hooks/useLocale";
import { DialogClose, DialogTrigger, Dialog, DialogContent } from "@components/Dialog";
import Slider from "@components/Slider";
import Button from "@components/ui/Button";
type ImageUploaderProps = {
id: string;

View File

@@ -1,49 +1,82 @@
import { AdminRequired } from "components/ui/AdminRequired";
import Link, { LinkProps } from "next/link";
import { useRouter } from "next/router";
import React, { ElementType, FC } from "react";
import React, { ElementType, FC, Fragment, MouseEventHandler } from "react";
import classNames from "@lib/classNames";
interface Props {
export interface NavTabProps {
tabs: {
name: string;
href: string;
/** If you want to change the path as per current tab */
href?: string;
/** If you want to change query param tabName as per current tab */
tabName?: string;
icon?: ElementType;
adminRequired?: boolean;
}[];
linkProps?: Omit<LinkProps, "href">;
}
const NavTabs: FC<Props> = ({ tabs, linkProps }) => {
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
const router = useRouter();
return (
<>
<nav
className="-mb-px flex space-x-2 space-x-5 rtl:space-x-reverse sm:rtl:space-x-reverse"
aria-label="Tabs">
className="-mb-px flex space-x-5 rtl:space-x-reverse sm:rtl:space-x-reverse"
aria-label="Tabs"
{...props}>
{tabs.map((tab) => {
const isCurrent = router.asPath === tab.href;
let href: string;
let isCurrent;
if ((tab.tabName && tab.href) || (!tab.tabName && !tab.href)) {
throw new Error("Use either tabName or href");
}
if (tab.href) {
href = tab.href;
isCurrent = router.asPath === tab.href;
} else if (tab.tabName) {
href = "";
isCurrent = router.query.tabName === tab.tabName;
}
const onClick: MouseEventHandler = tab.tabName
? (e) => {
e.preventDefault();
router.push({
query: {
...router.query,
tabName: tab.tabName,
},
});
}
: () => {};
const Component = tab.adminRequired ? AdminRequired : Fragment;
return (
<Link key={tab.name} href={tab.href} {...linkProps}>
<a
className={classNames(
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"
)}
aria-current={isCurrent ? "page" : undefined}>
{tab.icon && (
<tab.icon
className={classNames(
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
"-ml-0.5 hidden h-5 w-5 ltr:mr-2 rtl:ml-2 sm:inline-block"
)}
aria-hidden="true"
/>
)}
<span>{tab.name}</span>
</a>
</Link>
<Component key={tab.name}>
<Link key={tab.name} href={href!} {...linkProps}>
<a
onClick={onClick}
className={classNames(
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"
)}
aria-current={isCurrent ? "page" : undefined}>
{tab.icon && (
<tab.icon
className={classNames(
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
"-ml-0.5 hidden h-5 w-5 ltr:mr-2 rtl:ml-2 sm:inline-block"
)}
aria-hidden="true"
/>
)}
<span>{tab.name}</span>
</a>
</Link>
</Component>
);
})}
</nav>

View File

@@ -1,9 +1,9 @@
import { CreditCardIcon, KeyIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
import { CreditCardIcon, KeyIcon, LockClosedIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
import React from "react";
import { useLocale } from "@lib/hooks/useLocale";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import NavTabs from "./NavTabs";
import NavTabs, { NavTabProps } from "./NavTabs";
export default function SettingsShell({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
@@ -29,6 +29,12 @@ export default function SettingsShell({ children }: { children: React.ReactNode
href: "/settings/billing",
icon: CreditCardIcon,
},
{
name: t("admin"),
href: "/settings/admin",
icon: LockClosedIcon,
adminRequired: true,
},
];
return (

View File

@@ -1,78 +1,76 @@
import { SelectorIcon } from "@heroicons/react/outline";
import {
CalendarIcon,
ArrowLeftIcon,
CalendarIcon,
ClockIcon,
CogIcon,
ExternalLinkIcon,
LinkIcon,
LogoutIcon,
PuzzleIcon,
MoonIcon,
MapIcon,
MoonIcon,
ViewGridIcon,
} from "@heroicons/react/solid";
import { signOut, useSession } from "next-auth/react";
import { UserPlan } from "@prisma/client";
import { SessionContextValue, signOut, useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { ReactNode, useEffect, useState } from "react";
import React, { Fragment, ReactNode, useEffect } from "react";
import { Toaster } from "react-hot-toast";
import { useIsEmbed } from "@calcom/embed-core";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@calcom/ui/Dropdown";
import LicenseBanner from "@ee/components/LicenseBanner";
import TrialBanner from "@ee/components/TrialBanner";
import IntercomMenuItem from "@ee/lib/intercom/IntercomMenuItem";
import ZendeskMenuItem from "@ee/lib/zendesk/ZendeskMenuItem";
import HelpMenuItem from "@ee/components/support/HelpMenuItem";
import classNames from "@lib/classNames";
import { NEXT_PUBLIC_BASE_URL } from "@lib/config/constants";
import { WEBAPP_URL } from "@lib/config/constants";
import { shouldShowOnboarding } from "@lib/getting-started";
import { useLocale } from "@lib/hooks/useLocale";
import useMeQuery from "@lib/hooks/useMeQuery";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@components/ui/Dropdown";
import ImpersonatingBanner from "@components/ui/ImpersonatingBanner";
import pkg from "../package.json";
import { useViewerI18n } from "./I18nLanguageHandler";
import Logo from "./Logo";
import Button from "./ui/Button";
export function useMeQuery() {
const meQuery = trpc.useQuery(["viewer.me"], {
retry(failureCount) {
return failureCount > 3;
},
});
return meQuery;
}
function useRedirectToLoginIfUnauthenticated() {
function useRedirectToLoginIfUnauthenticated(isPublic = false) {
const { data: session, status } = useSession();
const loading = status === "loading";
const router = useRouter();
useEffect(() => {
if (isPublic) {
return;
}
if (!loading && !session) {
router.replace({
pathname: "/auth/login",
query: {
callbackUrl: `${NEXT_PUBLIC_BASE_URL}/${location.pathname}${location.search}`,
callbackUrl: `${WEBAPP_URL}${location.pathname}${location.search}`,
},
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading, session]);
}, [loading, session, isPublic]);
return {
loading: loading && !session,
session,
};
}
@@ -81,11 +79,7 @@ function useRedirectToOnboardingIfNeeded() {
const query = useMeQuery();
const user = query.data;
const [isRedirectingToOnboarding, setRedirecting] = useState(false);
useEffect(() => {
user && setRedirecting(shouldShowOnboarding(user));
}, [router, user]);
const isRedirectingToOnboarding = user && shouldShowOnboarding(user);
useEffect(() => {
if (isRedirectingToOnboarding) {
@@ -119,25 +113,15 @@ export function ShellSubHeading(props: {
);
}
export default function Shell(props: {
centered?: boolean;
title?: string;
heading: ReactNode;
subtitle?: ReactNode;
children: ReactNode;
CTA?: ReactNode;
HeadingLeftIcon?: ReactNode;
backPath?: string; // renders back button to specified path
// use when content needs to expand with flex
flexChildrenContainer?: boolean;
}) {
const { t } = useLocale();
const Layout = ({
status,
plan,
...props
}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => {
const isEmbed = useIsEmbed();
const router = useRouter();
const { loading } = useRedirectToLoginIfUnauthenticated();
const { isRedirectingToOnboarding } = useRedirectToOnboardingIfNeeded();
const telemetry = useTelemetry();
const { t } = useLocale();
const navigation = [
{
name: t("event_types_page_title"),
@@ -158,10 +142,22 @@ export default function Shell(props: {
current: router.asPath.startsWith("/availability"),
},
{
name: t("integrations"),
href: "/integrations",
icon: PuzzleIcon,
current: router.asPath.startsWith("/integrations"),
name: t("apps"),
href: "/apps",
icon: ViewGridIcon,
current: router.asPath.startsWith("/apps"),
child: [
{
name: t("app_store"),
href: "/apps",
current: router.asPath === "/apps",
},
{
name: t("installed_apps"),
href: "/apps/installed",
current: router.asPath === "/apps/installed",
},
],
},
{
name: t("settings"),
@@ -170,31 +166,10 @@ export default function Shell(props: {
current: router.asPath.startsWith("/settings"),
},
];
useEffect(() => {
telemetry.withJitsu((jitsu) => {
return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.asPath));
});
}, [telemetry, router.asPath]);
const pageTitle = typeof props.heading === "string" ? props.heading : props.title;
const query = useMeQuery();
const user = query.data;
const i18n = useViewerI18n();
if (i18n.status === "loading" || isRedirectingToOnboarding || loading) {
// show spinner whilst i18n is loading to avoid language flicker
return (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
<Loader />
</div>
);
}
return (
<>
<CustomBranding lightVal={user?.brandColor} darkVal={user?.darkBrandColor} />
<HeadSeo
title={pageTitle ?? "Cal.com"}
description={props.subtitle ? props.subtitle?.toString() : ""}
@@ -207,96 +182,129 @@ export default function Shell(props: {
<Toaster position="bottom-right" />
</div>
<div className="flex h-screen overflow-hidden bg-gray-100" data-testid="dashboard-shell">
<div className="hidden md:flex lg:flex-shrink-0">
<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>
{/* logo icon for tablet */}
<Link href="/event-types">
<a className="md:inline lg:hidden">
<Logo small icon />
</a>
</Link>
<nav className="mt-2 flex-1 space-y-1 bg-white px-2 lg:mt-5">
{navigation.map((item) => (
<Link key={item.name} href={item.href}>
<a
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
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>
</a>
</Link>
))}
</nav>
<div
className={classNames("flex h-screen overflow-hidden", props.large ? "bg-white" : "bg-gray-100")}
data-testid="dashboard-shell">
{status === "authenticated" && (
<div style={isEmbed ? { display: "none" } : {}} className="hidden md:flex lg:flex-shrink-0">
<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>
{/* logo icon for tablet */}
<Link href="/event-types">
<a className="md:inline lg:hidden">
<Logo small icon />
</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
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
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>
</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>
))}
</nav>
</div>
<TrialBanner />
<div
className="rounded-sm pb-2 pl-3 pt-2 pr-2 hover:bg-gray-100 lg:mx-2 lg:pl-2"
data-testid="user-dropdown-trigger">
<span className="hidden lg:inline">
<UserDropdown />
</span>
<span className="hidden md:inline lg:hidden">
<UserDropdown small />
</span>
</div>
<small style={{ fontSize: "0.5rem" }} className="mx-3 mt-1 mb-2 hidden opacity-50 lg:block">
&copy; {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"}
{process.env.NEXT_PUBLIC_WEBSITE_URL === "https://cal.com" ? "h" : "sh"}
<span className="lowercase" data-testid={`plan-${plan?.toLowerCase()}`}>
-{plan}
</span>
</small>
</div>
<TrialBanner />
<div className="rounded-sm pt-2 pb-2 pl-3 pr-2 hover:bg-gray-100 lg:mx-2 lg:pl-2">
<span className="hidden lg:inline">
<UserDropdown />
</span>
<span className="hidden md:inline lg:hidden">
<UserDropdown small />
</span>
</div>
<small style={{ fontSize: "0.5rem" }} className="mx-3 mt-1 mb-2 hidden opacity-50 lg:block">
&copy; {new Date().getFullYear()} Cal.com, Inc. v.{pkg.version + "-"}
{process.env.NEXT_PUBLIC_APP_URL === "https://cal.com" ? "h" : "sh"}
<span className="lowercase">-{user && user.plan}</span>
</small>
</div>
</div>
</div>
)}
<div className="flex w-0 flex-1 flex-col overflow-hidden">
<main
className={classNames(
"relative z-0 max-w-[1700px] flex-1 overflow-y-auto focus:outline-none",
"relative z-0 flex-1 overflow-y-auto focus:outline-none",
status === "authenticated" && "max-w-[1700px]",
props.flexChildrenContainer && "flex flex-col"
)}>
{/* show top navigation for md and smaller (tablet and phones) */}
<nav className="flex items-center justify-between border-b border-gray-200 bg-white p-4 md:hidden">
<Link href="/event-types">
<a>
<Logo />
</a>
</Link>
<div className="flex items-center gap-3 self-center">
<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>
<Link href="/settings/profile">
<a>
<CogIcon className="h-6 w-6" aria-hidden="true" />
</a>
</Link>
</button>
<UserDropdown small />
</div>
</nav>
{status === "authenticated" && (
<nav
style={isEmbed ? { display: "none" } : {}}
className="flex items-center justify-between border-b border-gray-200 bg-white p-4 md:hidden">
<Link href="/event-types">
<a>
<Logo />
</a>
</Link>
<div className="flex items-center gap-3 self-center">
<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>
<Link href="/settings/profile">
<a>
<CogIcon className="h-6 w-6" aria-hidden="true" />
</a>
</Link>
</button>
<UserDropdown small />
</div>
</nav>
)}
<div
className={classNames(
props.centered && "mx-auto md:max-w-5xl",
props.flexChildrenContainer && "flex flex-1 flex-col",
"py-8"
!props.large && "py-8"
)}>
<ImpersonatingBanner />
{!!props.backPath && (
<div className="mx-3 mb-8 sm:mx-8">
<Button
@@ -307,50 +315,73 @@ export default function Shell(props: {
</Button>
</div>
)}
<div className="block min-h-[80px] 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">
<h1 className="font-cal mb-1 text-xl text-gray-900">{props.heading}</h1>
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
{props.heading && (
<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"
)}>
{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>
</>
) : (
<>
<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>
</>
)}
</div>
{props.CTA && <div className="mb-4 flex-shrink-0">{props.CTA}</div>}
</div>
<div className="mb-4 flex-shrink-0">{props.CTA}</div>
</div>
)}
<div
className={classNames(
"px-4 sm:px-6 md:px-8",
props.flexChildrenContainer && "flex flex-1 flex-col"
)}>
{props.children}
{!props.isLoading ? props.children : props.customLoader}
</div>
{/* show bottom navigation for md and smaller (tablet and phones) */}
<nav 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" ? (
[]
) : (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.current ? "text-gray-900" : "text-neutral-400 hover:text-gray-700",
itemIdx === 0 ? "rounded-l-lg" : "",
itemIdx === navigation.length - 1 ? "rounded-r-lg" : "",
"group relative min-w-0 flex-1 overflow-hidden bg-white py-2 px-2 text-center text-xs font-medium hover:bg-gray-50 focus:z-10 sm:text-sm"
)}
aria-current={item.current ? "page" : undefined}>
<item.icon
{status === "authenticated" && (
<nav
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" ? (
[]
) : (
<Link key={item.name} href={item.href}>
<a
className={classNames(
item.current ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
"mx-auto mb-1 block h-5 w-5 flex-shrink-0 text-center"
item.current ? "text-gray-900" : "text-neutral-400 hover:text-gray-700",
itemIdx === 0 ? "rounded-l-lg" : "",
itemIdx === navigation.length - 1 ? "rounded-r-lg" : "",
"group relative min-w-0 flex-1 overflow-hidden bg-white py-2 px-2 text-center text-xs font-medium hover:bg-gray-50 focus:z-10 sm:text-sm"
)}
aria-hidden="true"
/>
<span className="truncate">{item.name}</span>
</a>
</Link>
)
)}
</nav>
aria-current={item.current ? "page" : undefined}>
<item.icon
className={classNames(
item.current ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
"mx-auto mb-1 block h-5 w-5 flex-shrink-0 text-center"
)}
aria-hidden="true"
/>
<span className="truncate">{item.name}</span>
</a>
</Link>
)
)}
</nav>
)}
{/* add padding to content for mobile navigation*/}
<div className="block pt-12 md:hidden" />
</div>
@@ -360,6 +391,63 @@ export default function Shell(props: {
</div>
</>
);
};
const MemoizedLayout = React.memo(Layout);
type LayoutProps = {
centered?: boolean;
title?: string;
heading?: ReactNode;
subtitle?: ReactNode;
children: ReactNode;
CTA?: ReactNode;
large?: boolean;
HeadingLeftIcon?: ReactNode;
backPath?: string; // renders back button to specified path
// use when content needs to expand with flex
flexChildrenContainer?: boolean;
isPublic?: boolean;
customLoader?: ReactNode;
};
export default function Shell(props: LayoutProps) {
const router = useRouter();
const { loading, session } = useRedirectToLoginIfUnauthenticated(props.isPublic);
const { isRedirectingToOnboarding } = useRedirectToOnboardingIfNeeded();
const telemetry = useTelemetry();
useEffect(() => {
telemetry.withJitsu((jitsu) => {
return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.asPath));
});
}, [telemetry, router.asPath]);
const query = useMeQuery();
const user = query.data;
const i18n = useViewerI18n();
const { status } = useSession();
const isLoading =
i18n.status === "loading" || query.status === "loading" || isRedirectingToOnboarding || loading;
if (isLoading) {
return (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
<Loader />
</div>
);
}
if (!session && !props.isPublic) return null;
return (
<>
<CustomBranding lightVal={user?.brandColor} darkVal={user?.darkBrandColor} />
<MemoizedLayout plan={user?.plan} status={status} {...props} isLoading={isLoading} />
</>
);
}
function UserDropdown({ small }: { small?: boolean }) {
@@ -376,7 +464,7 @@ function UserDropdown({ small }: { small?: boolean }) {
return (
<Dropdown>
<DropdownMenuTrigger asChild>
<div className="group flex w-full cursor-pointer appearance-none items-center">
<button className="group flex w-full cursor-pointer appearance-none items-center text-left">
<span
className={classNames(
small ? "h-8 w-8" : "h-10 w-10",
@@ -384,12 +472,7 @@ function UserDropdown({ small }: { small?: boolean }) {
)}>
<img
className="rounded-full"
src={
(process.env.NEXT_PUBLIC_APP_URL || process.env.NEXT_PUBLIC_BASE_URL) +
"/" +
user?.username +
"/avatar.png"
}
src={process.env.NEXT_PUBLIC_WEBSITE_URL + "/" + user?.username + "/avatar.png"}
alt={user?.username || "Nameless User"}
/>
{!user?.away && (
@@ -415,7 +498,7 @@ function UserDropdown({ small }: { small?: boolean }) {
/>
</span>
)}
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent portalled={true}>
<DropdownMenuItem>
@@ -424,7 +507,7 @@ function UserDropdown({ small }: { small?: boolean }) {
mutation.mutate({ away: !user?.away });
utils.invalidateQueries("viewer.me");
}}
className="flex cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">
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
@@ -443,7 +526,7 @@ function UserDropdown({ small }: { small?: boolean }) {
<a
target="_blank"
rel="noopener noreferrer"
href={`${process.env.NEXT_PUBLIC_APP_URL}/${user.username}`}
href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}`}
className="flex items-center px-4 py-2 text-sm text-gray-700">
<ExternalLinkIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("view_public_page")}
</a>
@@ -490,8 +573,9 @@ function UserDropdown({ small }: { small?: boolean }) {
<MapIcon className="h-5 w-5 text-gray-500 ltr:mr-3 rtl:ml-3" /> {t("visit_roadmap")}
</a>
</DropdownMenuItem>
<IntercomMenuItem />
<ZendeskMenuItem />
<HelpMenuItem />
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<a

View File

@@ -0,0 +1,54 @@
import { InformationCircleIcon } from "@heroicons/react/outline";
import { Trans } from "next-i18next";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
import { useLocale } from "@lib/hooks/useLocale";
export function UpgradeToProDialog({
modalOpen,
setModalOpen,
children,
}: {
modalOpen: boolean;
setModalOpen: (open: boolean) => void;
children: React.ReactNode;
}) {
const { t } = useLocale();
return (
<Dialog open={modalOpen}>
<DialogContent>
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100">
<InformationCircleIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />
</div>
<div className="mb-4 sm:flex sm:items-start">
<div className="mt-3 sm:mt-0 sm:text-left">
<h3 className="font-cal text-lg font-bold leading-6 text-gray-900" id="modal-title">
{t("only_available_on_pro_plan")}
</h3>
</div>
</div>
<div className="flex flex-col space-y-3">
<p>{children}</p>
<p>
<Trans i18nKey="plan_upgrade_instructions">
You can
<a href="/api/upgrade" className="underline">
upgrade here
</a>
.
</Trans>
</p>
</div>
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
<DialogClose asChild>
<Button className="btn-wide table-cell text-center" onClick={() => setModalOpen(false)}>
{t("dismiss")}
</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,27 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import AppCard from "./AppCard";
export default function AllApps({ apps }: { apps: App[] }) {
const { t } = useLocale();
return (
<div className="mb-16">
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("all_apps")}</h2>
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
{apps.map((app) => (
<AppCard
key={app.name}
name={app.name}
slug={app.slug}
description={app.description}
logo={app.logo}
rating={app.rating}
reviews={app.reviews}
/>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { StarIcon } from "@heroicons/react/solid";
import Link from "next/link";
import Button from "@calcom/ui/Button";
interface AppCardProps {
logo: string;
name: string;
slug?: string;
category?: string;
description: string;
rating: number;
reviews?: number;
}
export default function AppCard(props: AppCardProps) {
return (
<Link href={"/apps/" + props.slug}>
<a className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50">
<div className="flex">
<img src={props.logo} alt={props.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
<Button
data-testid={`app-store-app-card-${props.slug}`}
color="secondary"
className="ml-auto flex self-start"
onClick={() => {
console.log("The magic is supposed to happen here");
}}>
Add
</Button>
</div>
<h3 className="font-medium">{props.name}</h3>
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
</div> */}
<p className="mt-2 truncate text-sm text-gray-500">{props.description}</p>
</a>
</Link>
);
}

View File

@@ -0,0 +1,43 @@
import Image from "next/image";
import Link from "next/link";
import { useLocale } from "@calcom/lib/hooks/useLocale";
export default function AppStoreCategories({
categories,
}: {
categories: {
name: string;
count: number;
}[];
}) {
const { t } = useLocale();
return (
<div className="mb-16">
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("popular_categories")}</h2>
<div className="grid-col-1 grid w-full gap-3 overflow-scroll sm:grid-flow-col">
{categories.map((category) => (
<Link key={category.name} href={"/apps/categories/" + category.name}>
<a
data-testid={`app-store-category-${category.name}`}
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
<div className="min-w-24 -ml-5 text-center sm:ml-0">
<Image
alt={category.name}
width="352"
height="252"
layout="responsive"
src={"/app-store/" + category.name + ".svg"}
/>
</div>
<div className="self-center">
<h3 className="font-medium capitalize">{category.name}</h3>
<p className="text-sm text-gray-500">{category.count} apps</p>
</div>
</a>
</Link>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import React from "react";
import { SkeletonAvatar, SkeletonText } from "@calcom/ui";
import { ShellSubHeading } from "@components/Shell";
function SkeletonLoader() {
return (
<>
<ShellSubHeading title={<div className="h-6 w-32 rounded-sm bg-gray-100"></div>} />
<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 />
<SkeletonItem />
</ul>
</>
);
}
export default SkeletonLoader;
function SkeletonItem() {
return (
<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>
<div className="space-y-2">
<SkeletonText height="4" width="32"></SkeletonText>
<SkeletonText height="4" width="16"></SkeletonText>
</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>
</div>
</div>
</li>
);
}

View File

@@ -0,0 +1,78 @@
import Glide, { Options } from "@glidejs/glide";
import "@glidejs/glide/dist/css/glide.core.min.css";
import "@glidejs/glide/dist/css/glide.theme.min.css";
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/solid";
import { useEffect, useRef } from "react";
const Slider = <T extends unknown>({
title = "",
className = "",
items,
itemKey = (item) => `${item}`,
renderItem,
options = {},
}: {
title?: string;
className?: string;
items: T[];
itemKey?: (item: T) => string;
renderItem?: (item: T) => JSX.Element;
options?: Options;
}) => {
const glide = useRef(null);
const slider = useRef<Glide.Properties | null>(null);
useEffect(() => {
if (glide.current) {
slider.current = new Glide(glide.current, {
type: "carousel",
...options,
}).mount();
}
return () => slider.current?.destroy();
}, [options]);
return (
<div className={`mb-2 ${className}`}>
<style jsx global>
{`
.glide__slide {
height: auto !important;
}
`}
</style>
<div className="glide" ref={glide}>
<div className="flex cursor-default">
{title && (
<div>
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">{title}</h2>
</div>
)}
<div className="glide__arrows ml-auto" data-glide-el="controls">
<button data-glide-dir="<" className="mr-4">
<ArrowLeftIcon className="h-5 w-5 text-gray-600 hover:text-black" />
</button>
<button data-glide-dir=">">
<ArrowRightIcon className="h-5 w-5 text-gray-600 hover:text-black" />
</button>
</div>
</div>
<div className="glide__track" data-glide-el="track">
<ul className="glide__slides">
{items.map((item) => {
if (typeof renderItem !== "function") return null;
return (
<li key={itemKey(item)} className="glide__slide h-auto pl-0">
{renderItem(item)}
</li>
);
})}
</ul>
</div>
</div>
</div>
);
};
export default Slider;

View File

@@ -0,0 +1,39 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import AppCard from "./AppCard";
import Slider from "./Slider";
const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
const { t } = useLocale();
return (
<Slider<T>
className="mb-16"
title={t("trending_apps")}
items={items.filter((app) => !!app.trending)}
itemKey={(app) => app.name}
options={{
perView: 3,
breakpoints: {
768 /* and below */: {
perView: 1,
},
},
}}
renderItem={(app) => (
<AppCard
key={app.name}
name={app.name}
slug={app.slug}
description={app.description}
logo={app.logo}
rating={app.rating}
reviews={app.reviews}
/>
)}
/>
);
};
export default TrendingAppsSlider;

View File

@@ -2,12 +2,12 @@ import { signIn } from "next-auth/react";
import { Dispatch, SetStateAction } from "react";
import { useFormContext } from "react-hook-form";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
import Button from "@components/ui/Button";
interface Props {
email: string;
samlTenantID: string;

View File

@@ -2,9 +2,9 @@ import React, { useEffect, useState } from "react";
import useDigitInput from "react-digit-input";
import { useFormContext } from "react-hook-form";
import { useLocale } from "@lib/hooks/useLocale";
import { Input } from "@calcom/ui/form/fields";
import { Input } from "@components/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
export default function TwoFactor() {
const [value, onChange] = useState("");
@@ -29,8 +29,15 @@ export default function TwoFactor() {
<div className="mx-auto !mt-0 max-w-sm">
<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
<div className="flex flex-row space-x-1">
<Input className={className} name="2fa1" inputMode="decimal" {...digits[0]} autoFocus />
<div className="flex flex-row justify-between">
<Input
className={className}
name="2fa1"
inputMode="decimal"
{...digits[0]}
autoFocus
autoComplete="one-time-code"
/>
<Input className={className} name="2fa2" inputMode="decimal" {...digits[1]} />
<Input className={className} name="2fa3" inputMode="decimal" {...digits[2]} />
<Input className={className} name="2fa4" inputMode="decimal" {...digits[3]} />

View File

@@ -0,0 +1,89 @@
import { PlusIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { Button } from "@calcom/ui";
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
import { Form, TextField } from "@calcom/ui/form/fields";
import { HttpError } from "@lib/core/http/error";
import { trpc } from "@lib/trpc";
export function NewScheduleButton({ name = "new-schedule" }: { name?: string }) {
const router = useRouter();
const { t } = useLocale();
const form = useForm<{
name: string;
}>();
const { register } = form;
const createMutation = trpc.useMutation("viewer.availability.schedule.create", {
onSuccess: async ({ schedule }) => {
await router.push("/availability/" + schedule.id);
showToast(t("schedule_created_successfully", { scheduleName: schedule.name }), "success");
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
if (err.data?.code === "UNAUTHORIZED") {
const message = `${err.data.code}: You are not able to create this event`;
showToast(message, "error");
}
},
});
return (
<Dialog name={name} clearQueryParamsOnClose={["copy-schedule-id"]}>
<DialogTrigger asChild>
<Button data-testid={name} StartIcon={PlusIcon}>
{t("new_schedule_btn")}
</Button>
</DialogTrigger>
<DialogContent>
<div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{t("add_new_schedule")}
</h3>
<div>
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
</div>
</div>
<Form
form={form}
handleSubmit={(values) => {
createMutation.mutate(values);
}}>
<div className="mt-3 space-y-4">
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
{t("name")}
</label>
<div className="mt-1">
<input
type="text"
id="name"
required
className="block w-full rounded-sm border-gray-300 text-sm shadow-sm"
placeholder={t("default_schedule_name")}
{...register("name")}
/>
</div>
</div>
<div className="mt-8 flex flex-row-reverse gap-x-2">
<Button type="submit" loading={createMutation.isLoading}>
{t("continue")}
</Button>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
</div>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,338 @@
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, SingleValue } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import Dropdown, { DropdownMenuTrigger, DropdownMenuContent } from "@calcom/ui/Dropdown";
import { defaultDayRange } from "@lib/availability";
import { weekdayNames } from "@lib/core/i18n/weekday";
import useMeQuery from "@lib/hooks/useMeQuery";
import { TimeRange } from "@lib/types/schedule";
import Select from "@components/ui/form/Select";
dayjs.extend(utc);
dayjs.extend(timezone);
/** Begin Time Increments For Select */
const increment = 15;
type Option = {
readonly label: string;
readonly value: number;
};
/**
* Creates an array of times on a 15 minute interval from
* 00:00:00 (Start of day) to
* 23:45:00 (End of day with enough time for 15 min booking)
*/
const useOptions = () => {
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const { timeFormat } = query.data || { timeFormat: null };
const [filteredOptions, setFilteredOptions] = useState<Option[]>([]);
const options = useMemo(() => {
const end = dayjs().utc().endOf("day");
let t: Dayjs = dayjs().utc().startOf("day");
const options: Option[] = [];
while (t.isBefore(end)) {
options.push({
value: t.toDate().valueOf(),
label: dayjs(t)
.utc()
.format(timeFormat === 12 ? "h:mma" : "HH:mm"),
});
t = t.add(increment, "minutes");
}
return options;
}, []);
const filter = useCallback(
({ offset, limit, current }: { offset?: ConfigType; limit?: ConfigType; current?: ConfigType }) => {
if (current) {
setFilteredOptions([options.find((option) => option.value === dayjs(current).toDate().valueOf())!]);
} else
setFilteredOptions(
options.filter((option) => {
const time = dayjs(option.value);
return (!limit || time.isBefore(limit)) && (!offset || time.isAfter(offset));
})
);
},
[options]
);
return { options: filteredOptions, filter };
};
type TimeRangeFieldProps = {
name: string;
className?: string;
};
const LazySelect = ({
value,
min,
max,
...props
}: Omit<Props<Option, false, GroupBase<Option>>, "value"> & {
value: ConfigType;
min?: ConfigType;
max?: ConfigType;
}) => {
// Lazy-loaded options, otherwise adding a field has a noticable redraw delay.
const { options, filter } = useOptions();
useEffect(() => {
filter({ current: value });
}, [filter, value]);
return (
<Select
options={options}
onMenuOpen={() => {
if (min) filter({ offset: min });
if (max) filter({ limit: max });
}}
value={options.find((option) => option.value === dayjs(value).toDate().valueOf())}
onMenuClose={() => filter({ current: value })}
{...props}
/>
);
};
const TimeRangeField = ({ name, className }: TimeRangeFieldProps) => {
const { watch } = useFormContext();
const minEnd = watch(`${name}.start`);
const maxStart = watch(`${name}.end`);
return (
<div className={classNames("flex flex-grow items-center space-x-3", className)}>
<Controller
name={`${name}.start`}
render={({ field: { onChange, value } }) => {
return (
<LazySelect
className="w-[120px]"
value={value}
max={maxStart}
onChange={(option) => {
onChange(new Date(option?.value as number));
}}
/>
);
}}
/>
<span>-</span>
<Controller
name={`${name}.end`}
render={({ field: { onChange, value } }) => (
<LazySelect
className="flex-grow sm:w-[120px]"
value={value}
min={minEnd}
onChange={(option) => {
onChange(new Date(option?.value as number));
}}
/>
)}
/>
</div>
);
};
type ScheduleBlockProps = {
day: number;
weekday: string;
name: string;
};
const CopyTimes = ({ disabled, onApply }: { disabled: number[]; onApply: (selected: number[]) => void }) => {
const [selected, setSelected] = useState<number[]>([]);
const { i18n, t } = useLocale();
return (
<div className="m-4 space-y-2 py-4">
<p className="h6 text-xs font-medium uppercase text-neutral-400">Copy times to</p>
<ol className="space-y-2">
{weekdayNames(i18n.language).map((weekday, num) => (
<li key={weekday}>
<label className="flex w-full items-center justify-between">
<span>{weekday}</span>
<input
value={num}
defaultChecked={disabled.includes(num)}
disabled={disabled.includes(num)}
onChange={(e) => {
if (e.target.checked && !selected.includes(num)) {
setSelected(selected.concat([num]));
} else if (!e.target.checked && selected.includes(num)) {
setSelected(selected.slice(selected.indexOf(num), 1));
}
}}
type="checkbox"
className="inline-block rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400"
/>
</label>
</li>
))}
</ol>
<div className="pt-2">
<Button className="w-full justify-center" color="primary" onClick={() => onApply(selected)}>
{t("apply")}
</Button>
</div>
</div>
);
};
export const DayRanges = ({
name,
defaultValue = [defaultDayRange],
}: {
name: string;
defaultValue?: TimeRange[];
}) => {
const { setValue, watch } = useFormContext();
// XXX: Hack to make copying times work; `fields` is out of date until save.
const watcher = watch(name);
const { fields, replace, append, remove } = useFieldArray({
name,
});
useEffect(() => {
if (defaultValue.length && !fields.length) {
replace(defaultValue);
}
}, [replace, defaultValue, fields.length]);
const handleAppend = () => {
// FIXME: Fix type-inference, can't get this to work. @see https://github.com/react-hook-form/react-hook-form/issues/4499
const nextRangeStart = dayjs((fields[fields.length - 1] as unknown as TimeRange).end);
const nextRangeEnd = dayjs(nextRangeStart).add(1, "hour");
if (nextRangeEnd.isBefore(nextRangeStart.endOf("day"))) {
return append({
start: nextRangeStart.toDate(),
end: nextRangeEnd.toDate(),
});
}
};
return (
<div className="space-y-2">
{fields.map((field, index) => (
<div key={field.id} className="flex items-center rtl:space-x-reverse">
<div className="flex flex-grow sm:flex-grow-0">
<TimeRangeField name={`${name}.${index}`} />
<Button
size="icon"
color="minimal"
StartIcon={TrashIcon}
type="button"
onClick={() => remove(index)}
/>
</div>
{index === 0 && (
<div className="absolute top-2 right-0 text-right sm:relative sm:top-0 sm:flex-grow">
<Button
className="text-neutral-400"
type="button"
color="minimal"
size="icon"
StartIcon={PlusIcon}
onClick={handleAppend}
/>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
type="button"
color="minimal"
size="icon"
StartIcon={DuplicateIcon}
onClick={handleAppend}
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<CopyTimes
disabled={[parseInt(name.substring(name.lastIndexOf(".") + 1), 10)]}
onApply={(selected) =>
selected.forEach((day) => {
// TODO: Figure out why this is different?
// console.log(watcher, fields);
setValue(name.substring(0, name.lastIndexOf(".") + 1) + day, watcher);
})
}
/>
</DropdownMenuContent>
</Dropdown>
</div>
)}
</div>
))}
</div>
);
};
const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
const { t } = useLocale();
const form = useFormContext();
const watchAvailable = form.watch(`${name}.${day}`, []);
return (
<fieldset className="relative flex flex-col justify-between space-y-2 py-5 sm:flex-row sm:space-y-0">
<label
className={classNames(
"flex space-x-2 rtl:space-x-reverse",
!watchAvailable.length ? "w-full" : "w-1/3"
)}>
<div className={classNames(!watchAvailable.length ? "w-1/3" : "w-full")}>
<input
type="checkbox"
checked={watchAvailable.length}
onChange={(e) => {
form.setValue(`${name}.${day}`, e.target.checked ? [defaultDayRange] : []);
}}
className="inline-block rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500"
/>
<span className="ml-2 inline-block text-sm capitalize">{weekday}</span>
</div>
{!watchAvailable.length && (
<div className="flex-grow text-right text-sm text-gray-500 sm:flex-shrink">
{t("no_availability")}
</div>
)}
</label>
{!!watchAvailable.length && (
<div className="flex-grow">
<DayRanges name={`${name}.${day}`} defaultValue={[]} />
</div>
)}
</fieldset>
);
};
const Schedule = ({ name }: { name: string }) => {
const { i18n } = useLocale();
return (
<fieldset className="divide-y divide-gray-200">
{weekdayNames(i18n.language).map((weekday, num) => (
<ScheduleBlock key={num} name={name} weekday={weekday} day={num} />
))}
</fieldset>
);
};
export default Schedule;

View File

@@ -0,0 +1,33 @@
import React from "react";
import { SkeletonText } from "@calcom/ui";
function SkeletonLoader() {
return (
<ul className="animate-pulse divide-y divide-neutral-200 border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</ul>
);
}
export default SkeletonLoader;
function SkeletonItem() {
return (
<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>
</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>
</div>
</div>
</li>
);
}

View File

@@ -1,11 +1,14 @@
import { ExclamationIcon } from "@heroicons/react/solid";
import { SchedulingType } from "@prisma/client";
import { Dayjs } from "dayjs";
import dayjs, { Dayjs } from "dayjs";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { FC, useEffect, useState } from "react";
import { nameOfDay } from "@calcom/lib/weekday";
import classNames from "@lib/classNames";
import { timeZone } from "@lib/clock";
import { useLocale } from "@lib/hooks/useLocale";
import { useSlots } from "@lib/hooks/useSlots";
@@ -18,6 +21,8 @@ type AvailableTimesProps = {
afterBufferTime: number;
eventTypeId: number;
eventLength: number;
recurringCount: number | undefined;
eventTypeSlug: string;
slotInterval: number | null;
date: Dayjs;
users: {
@@ -30,8 +35,10 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
date,
eventLength,
eventTypeId,
eventTypeSlug,
slotInterval,
minimumBookingNotice,
recurringCount,
timeFormat,
users,
schedulingType,
@@ -41,7 +48,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
const { t, i18n } = useLocale();
const router = useRouter();
const { rescheduleUid } = router.query;
const { slots, loading, error } = useSlots({
date,
slotInterval,
@@ -63,9 +69,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
return (
<div className="mt-8 flex flex-col text-center sm:mt-0 sm:w-1/3 sm:pl-4 md:-mb-5">
<div className="mb-4 text-left text-lg font-light text-gray-600">
<span className="w-1/2 text-gray-600 dark:text-white">
<strong>{date.toDate().toLocaleString(i18n.language, { weekday: "long" })}</strong>
<span className="text-gray-500">
<span className="text-bookingdarker w-1/2 dark:text-white">
<strong>{nameOfDay(i18n.language, Number(date.format("d")))}</strong>
<span className="text-bookinglight">
{date.format(", D ")}
{date.toDate().toLocaleString(i18n.language, { month: "long" })}
</span>
@@ -85,6 +91,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
...router.query,
date: slot.time.format(),
type: eventTypeId,
slug: eventTypeSlug,
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
},
};
@@ -101,11 +110,11 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
<Link href={bookingUrl}>
<a
className={classNames(
"text-primary-500 hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 block rounded-sm border bg-white py-4 font-medium hover:text-white dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black",
"text-bookingdarker 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",
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
)}
data-testid="time">
{slot.time.format(timeFormat)}
{dayjs.tz(slot.time, timeZone()).format(timeFormat)}
</a>
</Link>
</div>

View File

@@ -1,22 +1,37 @@
import { BanIcon, CheckIcon, ClockIcon, XIcon } from "@heroicons/react/outline";
import { BanIcon, CheckIcon, ClockIcon, XIcon, PencilAltIcon } from "@heroicons/react/outline";
import { PaperAirplaneIcon } from "@heroicons/react/outline";
import { RefreshIcon } from "@heroicons/react/solid";
import { BookingStatus } from "@prisma/client";
import dayjs from "dayjs";
import { useState } from "react";
import { useMutation } from "react-query";
import { Frequency as RRuleFrequency } from "rrule";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { Tooltip } from "@calcom/ui/Tooltip";
import { TextArea } from "@calcom/ui/form/fields";
import { HttpError } from "@lib/core/http/error";
import { useLocale } from "@lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@lib/trpc";
import useMeQuery from "@lib/hooks/useMeQuery";
import { parseRecurringDates } from "@lib/parseDate";
import { inferQueryOutput, trpc, inferQueryInput } from "@lib/trpc";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@components/Dialog";
import { useMeQuery } from "@components/Shell";
import { TextArea } from "@components/form/fields";
import Button from "@components/ui/Button";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import TableActions, { ActionType } from "@components/ui/TableActions";
type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"];
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
function BookingListItem(booking: BookingItem) {
type BookingItemProps = BookingItem & {
listingStatus: BookingListingStatus;
recurringCount?: number;
};
function BookingListItem(booking: BookingItemProps) {
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
@@ -26,14 +41,22 @@ function BookingListItem(booking: BookingItem) {
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const mutation = useMutation(
async (confirm: boolean) => {
let body = {
id: booking.id,
confirmed: confirm,
language: i18n.language,
reason: rejectionReason,
};
/**
* Only pass down the recurring event id when we need to confirm the entire series, which happens in
* the "Upcoming" tab, to support confirming discretionally in the "Recurring" tab.
*/
if (booking.listingStatus === "upcoming" && booking.recurringEventId !== null) {
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
}
const res = await fetch("/api/book/confirm", {
method: "PATCH",
body: JSON.stringify({
id: booking.id,
confirmed: confirm,
language: i18n.language,
reason: rejectionReason,
}),
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
@@ -54,14 +77,20 @@ function BookingListItem(booking: BookingItem) {
const pendingActions: ActionType[] = [
{
id: "reject",
label: t("reject"),
label:
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("reject_all")
: t("reject"),
onClick: () => setRejectionDialogIsOpen(true),
icon: BanIcon,
disabled: mutation.isLoading,
},
{
id: "confirm",
label: t("confirm"),
label:
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
? t("confirm_all")
: t("confirm"),
onClick: () => mutation.mutate(true),
icon: CheckIcon,
disabled: mutation.isLoading,
@@ -79,15 +108,57 @@ function BookingListItem(booking: BookingItem) {
{
id: "reschedule",
label: t("reschedule"),
href: `/reschedule/${booking.uid}`,
icon: ClockIcon,
actions: [
{
id: "edit",
icon: PencilAltIcon,
label: t("edit_booking"),
href: `/reschedule/${booking.uid}`,
},
{
id: "reschedule_request",
icon: ClockIcon,
label: t("send_reschedule_request"),
onClick: () => setIsOpenRescheduleDialog(true),
},
],
},
];
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
const RequestSentMessage = () => {
return (
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
<PaperAirplaneIcon className="-mt-[1px] w-4 rotate-45" />
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
</div>
);
};
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
// Calculate the booking date(s)
let recurringStrings: string[] = [];
if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== null) {
[recurringStrings] = parseRecurringDates(
{
startDate: booking.startTime,
recurringEvent: booking.eventType.recurringEvent,
recurringCount: booking.recurringCount,
},
i18n
);
}
return (
<>
<RescheduleDialog
isOpenDialog={isOpenRescheduleDialog}
setIsOpenDialog={setIsOpenRescheduleDialog}
bookingUId={booking.uid}
/>
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
<DialogContent>
<DialogHeader title={t("rejection_reason_title")} />
@@ -121,12 +192,40 @@ function BookingListItem(booking: BookingItem) {
</Dialog>
<tr className="flex">
<td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell">
<td className="hidden whitespace-nowrap py-4 align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
</div>
<div className="text-sm text-gray-400">
{booking.recurringCount &&
booking.eventType?.recurringEvent?.freq &&
booking.listingStatus === "upcoming" && (
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
<div className="flex">
<Tooltip
content={recurringStrings.map((aDate, key) => (
<p key={key}>{aDate}</p>
))}>
<p className="text-gray-600 dark:text-white">
<RefreshIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
{`${t("every_for_freq", {
freq: t(
`${RRuleFrequency[booking.eventType.recurringEvent.freq]
.toString()
.toLowerCase()}`
),
})} ${booking.recurringCount} ${t(
`${RRuleFrequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`,
{ count: booking.recurringCount }
)}`}
</p>
</Tooltip>
</div>
</div>
)}
</div>
</td>
<td className={"flex-1 py-4 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}>
<div className="sm:hidden">
@@ -145,7 +244,10 @@ function BookingListItem(booking: BookingItem) {
</div>
<div
title={booking.title}
className="max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max">
className={classNames(
"max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max",
isCancelled ? "line-through" : ""
)}>
{booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>}
{booking.title}
{!!booking?.eventType?.price && !booking.paid && (
@@ -160,11 +262,17 @@ function BookingListItem(booking: BookingItem) {
&quot;{booking.description}&quot;
</div>
)}
{booking.attendees.length !== 0 && (
<div className="text-sm text-gray-900 hover:text-blue-500">
<a href={"mailto:" + booking.attendees[0].email}>{booking.attendees[0].email}</a>
</div>
)}
{isCancelled && booking.rescheduled && (
<div className="mt-2 inline-block text-left text-sm md:hidden">
<RequestSentMessage />
</div>
)}
</td>
<td className="whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">
@@ -179,6 +287,11 @@ function BookingListItem(booking: BookingItem) {
)}
</>
) : null}
{isCancelled && booking.rescheduled && (
<div className="hidden h-full items-center md:flex">
<RequestSentMessage />
</div>
)}
</td>
</tr>
</>

View File

@@ -5,13 +5,15 @@ import dayjsBusinessTime from "dayjs-business-time";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { memoize } from "lodash";
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useEmbedStyles } from "@calcom/embed-core";
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 { useLocale } from "@lib/hooks/useLocale";
import getSlots from "@lib/slots";
import { WorkingHours } from "@lib/types/schedule";
@@ -85,7 +87,8 @@ function DatePicker({
}: 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);
@@ -123,6 +126,8 @@ function DatePicker({
eventLength,
minimumBookingNotice,
workingHours,
}: Omit<DatePickerProps, "weekStart" | "onDatePicked" | "date"> & {
browsingDate: Dayjs;
}
) => {
const date = browsingDate.startOf("day").date(day);
@@ -185,7 +190,7 @@ function DatePicker({
batch: 1,
name: "DatePicker",
length: daysInMonth,
callback: (i: number, isLast) => {
callback: (i: number) => {
let day = i + 1;
days[daysInitialOffset + i] = {
disabled: isDisabledMemoized(day, {
@@ -229,20 +234,20 @@ function DatePicker({
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-800 md:w-1/3 "
? "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 text-gray-600">
<span className="w-1/2 text-gray-600 dark:text-white">
<strong className="text-gray-900 dark:text-white">{month}</strong>{" "}
<span className="text-gray-500">{year}</span>
<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 text-gray-600 dark:text-gray-400">
<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-gray-400 dark:text-gray-600"
isFirstMonth && "text-bookinglighter dark:text-gray-600"
)}
disabled={isFirstMonth}
data-testid="decrementMonth">
@@ -253,9 +258,9 @@ function DatePicker({
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-4 border-t border-b text-center dark:border-gray-800 sm:border-0">
<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="my-4 text-xs uppercase tracking-widest text-gray-500">
<div key={weekDay} className="text-bookinglight my-4 text-xs uppercase tracking-widest">
{weekDay}
</div>
))}
@@ -274,10 +279,15 @@ function DatePicker({
<button
onClick={() => onDatePicked(browsingDate.date(day.date))}
disabled={day.disabled}
style={
day.disabled ? { ...disabledDateButtonEmbedStyles } : { ...enabledDateButtonEmbedStyles }
}
className={classNames(
"absolute top-0 left-0 right-0 bottom-0 mx-auto w-full rounded-sm text-center",
"hover:border-brand hover:border dark:hover:border-white",
day.disabled ? "cursor-default font-light text-gray-400 hover:border-0" : "font-medium",
day.disabled
? "text-bookinglighter cursor-default font-light hover:border-0"
: "font-medium",
date && date.isSame(browsingDate.date(day.date), "day")
? "bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast"
: !day.disabled

View File

@@ -0,0 +1,39 @@
import React from "react";
import { SkeletonText } from "@calcom/ui";
import BookingsShell from "@components/BookingsShell";
function SkeletonLoader() {
return (
<ul className="mt-6 animate-pulse divide-y divide-neutral-200 border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</ul>
);
}
export default SkeletonLoader;
function SkeletonItem() {
return (
<li className="group flex w-full items-center justify-between px-2 py-4 sm:px-6">
<div className="flex-grow truncate text-sm">
<div className="flex">
<div className="flex flex-col space-y-2">
<SkeletonText width="32" height="5" />
<SkeletonText width="16" height="4" />
</div>
<SkeletonText width="24" height="5" className="ml-4" />
</div>
</div>
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
<SkeletonText width="16" height="6" />
<SkeletonText width="32" height="6" />
</div>
</div>
</li>
);
}

View File

@@ -1,11 +1,14 @@
// Get router variables
import {
ArrowLeftIcon,
CalendarIcon,
ChevronDownIcon,
ChevronUpIcon,
ClockIcon,
CreditCardIcon,
GlobeIcon,
InformationCircleIcon,
RefreshIcon,
} from "@heroicons/react/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useContracts } from "contexts/contractsContext";
@@ -15,13 +18,27 @@ import utc from "dayjs/plugin/utc";
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { Frequency as RRuleFrequency } from "rrule";
import {
useEmbedStyles,
useIsEmbed,
useIsBackgroundTransparent,
sdkActionManager,
useEmbedType,
useEmbedNonStylesConfig,
} from "@calcom/embed-core";
import classNames from "@calcom/lib/classNames";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { BASE_URL } from "@lib/config/constants";
import { useLocale } from "@lib/hooks/useLocale";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import useTheme from "@lib/hooks/useTheme";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { parseDate } from "@lib/parseDate";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
@@ -41,13 +58,18 @@ dayjs.extend(customParseFormat);
type Props = AvailabilityTeamPageProps | AvailabilityPageProps;
const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Props) => {
const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage, booking }: Props) => {
const router = useRouter();
const isEmbed = useIsEmbed();
const { rescheduleUid } = router.query;
const { isReady, Theme } = useTheme(profile.theme);
const { t } = useLocale();
const { t, i18n } = useLocale();
const { contracts } = useContracts();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
let isBackgroundTransparent = useIsBackgroundTransparent();
useExposePlanGlobally(plan);
useEffect(() => {
if (eventType.metadata.smartContractAddress) {
const eventOwner = eventType.users[0];
@@ -59,26 +81,42 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
const selectedDate = useMemo(() => {
const dateString = asStringOrNull(router.query.date);
if (dateString) {
// todo some extra validation maybe.
const utcOffsetAsDate = dayjs(dateString.substr(11, 14), "Hmm");
const utcOffset = parseInt(
dateString.substr(10, 1) + (utcOffsetAsDate.hour() * 60 + utcOffsetAsDate.minute())
);
const date = dayjs(dateString.substr(0, 10)).utcOffset(utcOffset, true);
const offsetString = dateString.substr(11, 14); // hhmm
const offsetSign = dateString.substr(10, 1); // + or -
const offsetHour = offsetString.slice(0, -2);
const offsetMinute = offsetString.slice(-2);
const utcOffsetInMinutes =
(offsetSign === "-" ? -1 : 1) *
(60 * (offsetHour !== "" ? parseInt(offsetHour) : 0) +
(offsetMinute !== "" ? parseInt(offsetMinute) : 0));
const date = dayjs(dateString.substr(0, 10)).utcOffset(utcOffsetInMinutes, true);
return date.isValid() ? date : null;
}
return null;
}, [router.query.date]);
if (selectedDate) {
// Let iframe take the width available due to increase in max-width
sdkActionManager?.fire("__refreshWidth", {});
}
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat);
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
const telemetry = useTelemetry();
useEffect(() => {
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.pageView, collectPageParameters()));
telemetry.withJitsu((jitsu) =>
jitsu.track(
telemetryEventTypes.pageView,
collectPageParameters("availability", { isTeamBooking: document.URL.includes("team/") })
)
);
}, [telemetry]);
const changeDate = (newDate: Dayjs) => {
@@ -108,6 +146,15 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
setTimeFormat(is24hClock ? "HH:mm" : "h:mma");
};
// Recurring event sidebar requires more space
const maxWidth = selectedDate
? recurringEventCount
? "max-w-6xl"
: "max-w-5xl"
: recurringEventCount
? "max-w-4xl"
: "max-w-3xl";
return (
<>
<Theme />
@@ -121,17 +168,25 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div>
<main
className={
"transition-max-width mx-auto my-0 duration-500 ease-in-out md:my-24 " +
(selectedDate ? "max-w-5xl" : "max-w-3xl")
}>
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed
? classNames(maxWidth)
: classNames("transition-max-width mx-auto my-0 duration-500 ease-in-out md:my-24", maxWidth)
)}>
{isReady && (
<div className="rounded-sm border-gray-200 bg-white dark:bg-gray-900 sm:dark:border-gray-600 md:border">
<div
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent ? "" : "bg-white dark:bg-gray-800 sm:dark:border-gray-600",
"border-bookinglightest rounded-md md:border",
isEmbed ? "mx-auto" : maxWidth
)}>
{/* mobile: details */}
<div className="block p-4 sm:p-8 md:hidden">
<div className="flex items-center">
<div>
<AvatarGroup
border="border-2 dark:border-gray-900 border-white"
border="border-2 dark:border-gray-800 border-white"
items={
[
{ image: profile.image, alt: profile.name, title: profile.name },
@@ -139,7 +194,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
.filter((user) => user.name !== profile.name)
.map((user) => ({
title: user.name,
image: `${process.env.NEXT_PUBLIC_APP_URL}/${user.username}/avatar.png`,
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`,
alt: user.name || undefined,
})),
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
@@ -147,17 +202,25 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
size={9}
truncateAfter={5}
/>
<div className="ltr:ml-3 rtl:mr-3">
<p className="text-sm font-medium text-black dark:text-gray-300">{profile.name}</p>
<div className="flex gap-2 text-xs font-medium text-gray-600">
{eventType.title}
<div>
<ClockIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
<div className="mt-4">
<p className="text-sm font-medium text-black dark:text-white">{profile.name}</p>
<div className="mt-2 gap-2 dark:text-gray-100">
<h1 className="text-bookingdark mb-4 text-xl font-semibold dark:text-white">
{eventType.title}
</h1>
{eventType?.description && (
<p className="text-bookinglight mb-2 dark:text-white">
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
{eventType.description}
</p>
)}
<p className="text-bookinglight mb-2 dark:text-white">
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
{eventType.length} {t("minutes")}
</div>
</p>
{eventType.price > 0 && (
<div>
<CreditCardIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
<div className="text-gray-600 dark:text-white">
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 dark:text-gray-400" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
@@ -167,20 +230,35 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
</IntlProvider>
</div>
)}
<div className="md:hidden">
{booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-8 mb-2 text-gray-600 dark:text-white"
data-testid="former_time_p_mobile">
{t("former_time")}
</p>
<p className="text-gray-500 line-through dark:text-white">
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{typeof booking.startTime === "string" &&
parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)}
</div>
</div>
</div>
</div>
<p className="mt-3 text-gray-600 dark:text-gray-200">{eventType.description}</p>
</div>
<div className="px-4 sm:flex sm:p-4 sm:py-5">
<div
className={
"hidden pr-8 sm:border-r sm:dark:border-gray-800 md:flex md:flex-col " +
(selectedDate ? "sm:w-1/3" : "sm:w-1/2")
"hidden pr-8 sm:border-r sm:dark:border-gray-700 md:flex md:flex-col " +
(selectedDate ? "sm:w-1/3" : recurringEventCount ? "sm:w-2/3" : "sm:w-1/2")
}>
<AvatarGroup
border="border-2 dark:border-gray-900 border-white"
border="border-2 dark:border-gray-800 border-white"
items={
[
{ image: profile.image, alt: profile.name, title: profile.name },
@@ -189,7 +267,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
.map((user) => ({
title: user.name,
alt: user.name,
image: `${process.env.NEXT_PUBLIC_APP_URL}/${user.username}/avatar.png`,
image: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user.username}/avatar.png`,
})),
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
}
@@ -197,16 +275,49 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
truncateAfter={3}
/>
<h2 className="mt-3 font-medium text-gray-500 dark:text-gray-300">{profile.name}</h2>
<h1 className="font-cal mb-4 text-3xl font-semibold text-gray-800 dark:text-white">
<h1 className="font-cal mb-4 text-xl font-semibold text-gray-900 dark:text-white">
{eventType.title}
</h1>
<p className="mb-1 -ml-2 px-2 py-1 text-gray-500">
<ClockIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
{eventType?.description && (
<p className="text-bookinglight mb-3 dark:text-white">
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{eventType.description}
</p>
)}
<p className="text-bookinglight mb-3 dark:text-white">
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
{eventType.length} {t("minutes")}
</p>
{!rescheduleUid && eventType.recurringEvent?.count && eventType.recurringEvent?.freq && (
<div className="mb-3 text-gray-600 dark:text-white">
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
<p className="mb-1 -ml-2 inline px-2 py-1">
{t("every_for_freq", {
freq: t(
`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
),
})}
</p>
<input
type="number"
min="1"
max={eventType.recurringEvent.count}
className="w-16 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"
defaultValue={eventType.recurringEvent.count}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
<p className="inline text-gray-600 dark:text-white">
{t(`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
count: recurringEventCount,
})}
</p>
</div>
)}
{eventType.price > 0 && (
<p className="mb-1 -ml-2 px-2 py-1 text-gray-500">
<CreditCardIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
<p className="mb-1 -ml-2 px-2 py-1 text-gray-600 dark:text-white">
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
@@ -218,9 +329,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
)}
<TimezoneDropdown />
<p className="mt-3 mb-8 text-gray-600 dark:text-gray-200">{eventType.description}</p>
{previousPage === `${BASE_URL}/${profile.slug}` && (
{previousPage === `${WEBAPP_URL}/${profile.slug}` && (
<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"
@@ -229,7 +338,21 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
<p className="sr-only">Go Back</p>
</div>
)}
{booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-4 mb-3 text-gray-600 dark:text-white"
data-testid="former_time_p_desktop">
{t("former_time")}
</p>
<p className="text-gray-500 line-through dark:text-white">
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)}
</div>
<DatePicker
date={selectedDate}
periodType={eventType?.periodType}
@@ -253,8 +376,10 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
timeFormat={timeFormat}
minimumBookingNotice={eventType.minimumBookingNotice}
eventTypeId={eventType.id}
eventTypeSlug={eventType.slug}
slotInterval={eventType.slotInterval}
eventLength={eventType.length}
recurringCount={recurringEventCount}
date={selectedDate}
users={eventType.users}
schedulingType={eventType.schedulingType ?? null}
@@ -265,7 +390,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
</div>
</div>
)}
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && <PoweredByCal />}
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
</main>
</div>
</>
@@ -274,8 +399,8 @@ const AvailabilityPage = ({ profile, eventType, workingHours, previousPage }: Pr
function TimezoneDropdown() {
return (
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
<Collapsible.Trigger className="min-w-32 mb-1 -ml-2 px-2 py-1 text-left text-gray-500">
<GlobeIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
<Collapsible.Trigger className="min-w-32 mb-1 -ml-2 px-2 py-1 text-left 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" />

View File

@@ -1,4 +1,13 @@
import { CalendarIcon, ClockIcon, CreditCardIcon, ExclamationIcon } from "@heroicons/react/solid";
import {
CalendarIcon,
ClockIcon,
CreditCardIcon,
ExclamationCircleIcon,
ExclamationIcon,
InformationCircleIcon,
RefreshIcon,
} from "@heroicons/react/solid";
import { zodResolver } from "@hookform/resolvers/zod";
import { EventTypeCustomInputType } from "@prisma/client";
import { useContracts } from "contexts/contractsContext";
import dayjs from "dayjs";
@@ -6,39 +15,49 @@ import { useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router";
import React, { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { FormattedNumber, IntlProvider } from "react-intl";
import { ReactMultiEmail } from "react-multi-email";
import { useMutation } from "react-query";
import { Frequency as RRuleFrequency } from "rrule";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { useEmbedNonStylesConfig, useIsBackgroundTransparent, useIsEmbed } from "@calcom/embed-core";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { createPaymentLink } from "@calcom/stripe/client";
import { Button } from "@calcom/ui/Button";
import { Tooltip } from "@calcom/ui/Tooltip";
import { EmailInput, Form } from "@calcom/ui/form/fields";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { ensureArray } from "@lib/ensureArray";
import { useLocale } from "@lib/hooks/useLocale";
import useTheme from "@lib/hooks/useTheme";
import { LocationType } from "@lib/location";
import createBooking from "@lib/mutations/bookings/create-booking";
import { parseZone } from "@lib/parseZone";
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
import { parseDate, parseRecurringDates } from "@lib/parseDate";
import slugify from "@lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import CustomBranding from "@components/CustomBranding";
import { EmailInput, Form } from "@components/form/fields";
import AvatarGroup from "@components/ui/AvatarGroup";
import { Button } from "@components/ui/Button";
import type PhoneInputType from "@components/ui/form/PhoneInput";
import { BookPageProps } from "../../../pages/[user]/book";
import { HashLinkPageProps } from "../../../pages/d/[link]/book";
import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
/** These are like 40kb that not every user needs */
const PhoneInput = dynamic(() => import("@components/ui/form/PhoneInput"));
const PhoneInput = dynamic(
() => import("@components/ui/form/PhoneInput")
) as unknown as typeof PhoneInputType;
type BookingPageProps = BookPageProps | TeamBookingPageProps;
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
type BookingFormValues = {
name: string;
@@ -52,11 +71,25 @@ type BookingFormValues = {
};
};
const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
const BookingPage = ({
eventType,
booking,
profile,
isDynamicGroupBooking,
recurringEventCount,
locationLabels,
hasHashedBookingLink,
hashedLink,
}: BookingPageProps) => {
const { t, i18n } = useLocale();
const isEmbed = useIsEmbed();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const router = useRouter();
const { contracts } = useContracts();
const { data: session } = useSession();
const isBackgroundTransparent = useIsBackgroundTransparent();
useEffect(() => {
if (eventType.metadata.smartContractAddress) {
const eventOwner = eventType.users[0];
@@ -69,7 +102,7 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { attendees, paymentUid } = responseData;
const { id, attendees, paymentUid } = responseData;
if (paymentUid) {
return await router.push(
createPaymentLink({
@@ -96,11 +129,45 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
query: {
date,
type: eventType.id,
eventSlug: eventType.slug,
user: profile.slug,
reschedule: !!rescheduleUid,
name: attendees[0].name,
email: attendees[0].email,
location,
eventName: profile.eventName || "",
bookingId: id,
},
});
},
});
const recurringMutation = useMutation(createRecurringBooking, {
onSuccess: async (responseData = []) => {
const { attendees = [], recurringEventId } = responseData[0] || {};
const location = (function humanReadableLocation(location) {
if (!location) {
return;
}
if (location.includes("integration")) {
return t("web_conferencing_details_to_follow");
}
return location;
})(responseData[0].location);
return router.push({
pathname: "/success",
query: {
date,
type: eventType.id,
eventSlug: eventType.slug,
recur: recurringEventId,
user: profile.slug,
reschedule: !!rescheduleUid,
name: attendees[0].name,
email: attendees[0].email,
location,
eventName: profile.eventName || "",
},
});
},
@@ -114,7 +181,7 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
const eventTypeDetail = { isWeb3Active: false, ...eventType };
type Location = { type: LocationType; address?: string };
type Location = { type: LocationType; address?: string; link?: string };
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
const locations: Location[] = useMemo(
() => (eventType.locations as Location[]) || [],
@@ -130,19 +197,11 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
const telemetry = useTelemetry();
const locationInfo = (type: LocationType) => locations.find((location) => location.type === type);
const loggedInIsOwner = eventType?.users[0]?.name === session?.user?.name;
const guestListEmails = !isDynamicGroupBooking
? booking?.attendees.slice(1).map((attendee) => attendee.email)
: [];
// TODO: Move to translations
const locationLabels = {
[LocationType.InPerson]: t("in_person_meeting"),
[LocationType.Phone]: t("phone_call"),
[LocationType.GoogleMeet]: "Google Meet",
[LocationType.Zoom]: "Zoom Video",
[LocationType.Jitsi]: "Jitsi Meet",
[LocationType.Daily]: "Daily.co Video",
[LocationType.Huddle01]: "Huddle01 Video",
[LocationType.Tandem]: "Tandem Video",
};
const loggedInIsOwner = eventType.users[0].name === session?.user.name;
const defaultValues = () => {
if (!rescheduleUid) {
return {
@@ -169,12 +228,21 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
return {
name: primaryAttendee.name || "",
email: primaryAttendee.email || "",
guests: booking.attendees.slice(1).map((attendee) => attendee.email),
guests: guestListEmails,
notes: booking.description || "",
};
};
const bookingFormSchema = z
.object({
name: z.string().min(1),
email: z.string().email(),
})
.passthrough();
const bookingForm = useForm<BookingFormValues>({
defaultValues: defaultValues(),
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
});
const selectedLocation = useWatch({
@@ -199,23 +267,35 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
case LocationType.InPerson: {
return locationInfo(locationType)?.address || "";
}
case LocationType.Link: {
return locationInfo(locationType)?.link || "";
}
// Catches all other location types, such as Google Meet, Zoom etc.
default:
return selectedLocation || "";
}
};
const parseDate = (date: string | null) => {
if (!date) return "No date";
const parsedZone = parseZone(date);
if (!parsedZone?.isValid()) return "Invalid date";
const formattedTime = parsedZone?.format(detectBrowserTimeFormat);
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
};
// Calculate the booking date(s)
let recurringStrings: string[] = [],
recurringDates: Date[] = [];
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
[recurringStrings, recurringDates] = parseRecurringDates(
{
startDate: date,
recurringEvent: eventType.recurringEvent,
recurringCount: parseInt(recurringEventCount.toString()),
},
i18n
);
}
const bookEvent = (booking: BookingFormValues) => {
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())
jitsu.track(
telemetryEventTypes.bookingConfirmed,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
)
);
// "metadata" is a reserved key to allow for connecting external users without relying on the email address.
@@ -232,7 +312,7 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
{}
);
let web3Details;
let web3Details: Record<"userWallet" | "userSignature", string> | undefined;
if (eventTypeDetail.metadata.smartContractAddress) {
web3Details = {
// @ts-ignore
@@ -241,27 +321,63 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
};
}
mutation.mutate({
...booking,
web3Details,
start: dayjs(date).format(),
end: dayjs(date).add(eventType.length, "minute").format(),
eventTypeId: eventType.id,
timeZone: timeZone(),
language: i18n.language,
rescheduleUid,
user: router.query.user,
location: getLocationValue(
booking.locationType ? booking : { ...booking, locationType: selectedLocation }
),
metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label,
value: booking.customInputs![inputId],
})),
});
if (recurringDates.length) {
// Identify set of bookings to one intance of recurring event to support batch changes
const recurringEventId = uuidv4();
const recurringBookings = recurringDates.map((recurringDate) => ({
...booking,
web3Details,
start: dayjs(recurringDate).format(),
end: dayjs(recurringDate).add(eventType.length, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
recurringEventId,
// Added to track down the number of actual occurrences selected by the user
recurringCount: recurringDates.length,
timeZone: timeZone(),
language: i18n.language,
rescheduleUid,
user: router.query.user,
location: getLocationValue(
booking.locationType ? booking : { ...booking, locationType: selectedLocation }
),
metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label,
value: booking.customInputs![inputId],
})),
hasHashedBookingLink,
hashedLink,
}));
recurringMutation.mutate(recurringBookings);
} else {
mutation.mutate({
...booking,
web3Details,
start: dayjs(date).format(),
end: dayjs(date).add(eventType.length, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
timeZone: timeZone(),
language: i18n.language,
rescheduleUid,
user: router.query.user,
location: getLocationValue(
booking.locationType ? booking : { ...booking, locationType: selectedLocation }
),
metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label,
value: booking.customInputs![inputId],
})),
hasHashedBookingLink,
hashedLink,
});
}
};
const disableInput = !!rescheduleUid;
return (
<div>
<Theme />
@@ -281,13 +397,24 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
<link rel="icon" href="/favicon.ico" />
</Head>
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<main className="mx-auto my-0 max-w-3xl rounded-sm sm:my-24 sm:border sm:dark:border-gray-600">
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "" : "sm:my-24",
"my-0 max-w-3xl "
)}>
{isReady && (
<div className="overflow-hidden border border-gray-200 bg-white dark:border-0 dark:bg-neutral-900 sm:rounded-sm">
<div
className={classNames(
"main overflow-hidden",
isEmbed ? "" : "border border-gray-200",
isBackgroundTransparent ? "" : "dark:border-1 bg-white dark:bg-gray-800",
"rounded-md sm:border sm:dark:border-gray-600"
)}>
<div className="px-4 py-5 sm:flex sm:p-4">
<div className="sm:w-1/2 sm:border-r sm:dark:border-gray-800">
<div className="sm:w-1/2 sm:border-r sm:dark:border-gray-700">
<AvatarGroup
border="border-2 border-white dark:border-gray-900"
border="border-2 border-white dark:border-gray-800"
size={14}
items={[{ image: profile.image || "", alt: profile.name || "" }].concat(
eventType.users
@@ -298,17 +425,25 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
}))
)}
/>
<h2 className="font-cal mt-2 font-medium text-gray-500 dark:text-gray-300">{profile.name}</h2>
<h1 className="mb-4 text-3xl font-semibold text-gray-800 dark:text-white">
<h2 className="font-cal text-bookinglight mt-2 font-medium dark:text-gray-300">
{profile.name}
</h2>
<h1 className="text-bookingdark mb-4 text-xl font-semibold dark:text-white">
{eventType.title}
</h1>
<p className="mb-2 text-gray-500">
<ClockIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
{eventType?.description && (
<p className="text-bookinglight mb-2 dark:text-white">
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{eventType.description}
</p>
)}
<p className="text-bookinglight mb-2 dark:text-white">
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
{eventType.length} {t("minutes")}
</p>
{eventType.price > 0 && (
<p className="mb-1 -ml-2 px-2 py-1 text-gray-500">
<CreditCardIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
<p className="text-bookinglight mb-1 -ml-2 px-2 py-1 dark:text-white">
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
@@ -318,18 +453,59 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
</IntlProvider>
</p>
)}
<p className="mb-4 text-green-500">
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
{parseDate(date)}
</p>
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="mb-3 text-gray-600 dark:text-white">
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
<p className="mb-1 -ml-2 inline px-2 py-1">
{`${t("every_for_freq", {
freq: t(`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`),
})} ${recurringEventCount} ${t(
`${RRuleFrequency[eventType.recurringEvent.freq].toString().toLowerCase()}`,
{ count: parseInt(recurringEventCount.toString()) }
)}`}
</p>
</div>
)}
<div className="text-bookinghighlight mb-4 flex">
<CalendarIcon className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
<div className="-mt-1">
{(rescheduleUid || !eventType.recurringEvent.freq) &&
parseDate(dayjs.tz(date, timeZone()), i18n)}
{!rescheduleUid &&
eventType.recurringEvent.freq &&
recurringStrings.slice(0, 5).map((aDate, key) => <p key={key}>{aDate}</p>)}
{!rescheduleUid && eventType.recurringEvent.freq && recurringStrings.length > 5 && (
<div className="flex">
<Tooltip
content={recurringStrings.slice(5).map((aDate, key) => (
<p key={key}>{aDate}</p>
))}>
<p className="text-gray-600 dark:text-white">
{t("plus_more", { count: recurringStrings.length - 5 })}
</p>
</Tooltip>
</div>
)}
</div>
</div>
{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && (
<p className="mb-1 -ml-2 px-2 py-1 text-gray-500">
<p className="text-bookinglight mb-1 -ml-2 px-2 py-1">
{t("requires_ownership_of_a_token") + " " + eventType.metadata.smartContractAddress}
</p>
)}
<p className="mb-8 text-gray-600 dark:text-white">{eventType.description}</p>
{booking?.startTime && rescheduleUid && (
<div>
<p className="mt-8 mb-2 text-gray-600 dark:text-white" data-testid="former_time_p">
{t("former_time")}
</p>
<p className="text-gray-500 line-through dark:text-white">
<CalendarIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)}
</div>
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
<div className="mt-8 sm:w-1/2 sm:pl-8 sm:pr-4">
<Form form={bookingForm} handleSubmit={bookEvent}>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
@@ -337,13 +513,17 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
</label>
<div className="mt-1">
<input
{...bookingForm.register("name")}
{...bookingForm.register("name", { required: true })}
type="text"
name="name"
id="name"
required
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-black dark:text-white dark:selection:bg-green-500 sm:text-sm"
className={classNames(
"focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm",
disableInput ? "bg-gray-200 dark:text-gray-500" : ""
)}
placeholder={t("example_name")}
disabled={disableInput}
/>
</div>
</div>
@@ -357,9 +537,23 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
<EmailInput
{...bookingForm.register("email")}
required
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-black dark:text-white dark:selection:bg-green-500 sm:text-sm"
className={classNames(
"focus:border-brand block w-full rounded-sm shadow-sm focus:ring-black dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm",
disableInput ? "bg-gray-200 dark:text-gray-500" : "",
bookingForm.formState.errors.email
? "border-red-700 focus:ring-red-700"
: " border-gray-300 dark:border-gray-900"
)}
placeholder="you@example.com"
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
disabled={disableInput}
/>
{bookingForm.formState.errors.email && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<ExclamationCircleIcon className="mr-2 h-3 w-3" />
<p>{t("email_validation_error")}</p>
</div>
)}
</div>
</div>
{locations.length > 1 && (
@@ -391,13 +585,13 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
{t("phone_number")}
</label>
<div className="mt-1">
<PhoneInput
// @ts-expect-error
<PhoneInput<BookingFormValues>
control={bookingForm.control}
name="phone"
placeholder={t("enter_phone_number")}
id="phone"
required
disabled={disableInput}
/>
</div>
</div>
@@ -420,8 +614,12 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
})}
id={"custom_" + input.id}
rows={3}
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-black dark:text-white dark:selection:bg-green-500 sm:text-sm"
className={classNames(
"focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm",
disableInput ? "bg-gray-200 dark:text-gray-500" : ""
)}
placeholder={input.placeholder}
disabled={disableInput}
/>
)}
{input.type === EventTypeCustomInputType.TEXT && (
@@ -431,8 +629,9 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
required: input.required,
})}
id={"custom_" + input.id}
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-black dark:text-white dark:selection:bg-green-500 sm:text-sm"
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm"
placeholder={input.placeholder}
disabled={disableInput}
/>
)}
{input.type === EventTypeCustomInputType.NUMBER && (
@@ -442,7 +641,7 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
required: input.required,
})}
id={"custom_" + input.id}
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-black dark:text-white dark:selection:bg-green-500 sm:text-sm"
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm"
placeholder=""
/>
)}
@@ -484,32 +683,49 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
{t("guests")}
</label>
<Controller
control={bookingForm.control}
name="guests"
render={({ field: { onChange, value } }) => (
<ReactMultiEmail
className="relative"
placeholder="guest@example.com"
emails={value}
onChange={onChange}
getLabel={(
email: string,
index: number,
removeEmail: (index: number) => void
) => {
return (
<div data-tag key={index}>
{email}
<span data-tag-handle onClick={() => removeEmail(index)}>
×
</span>
</div>
);
}}
/>
)}
/>
{!disableInput && (
<Controller
control={bookingForm.control}
name="guests"
render={({ field: { onChange, value } }) => (
<ReactMultiEmail
className="relative"
placeholder="guest@example.com"
emails={value}
onChange={onChange}
getLabel={(
email: string,
index: number,
removeEmail: (index: number) => void
) => {
return (
<div data-tag key={index} className="cursor-pointer">
{email}
{!disableInput && (
<span data-tag-handle onClick={() => removeEmail(index)}>
×
</span>
)}
</div>
);
}}
/>
)}
/>
)}
{/* Custom code when guest emails should not be editable */}
{disableInput && guestListEmails && guestListEmails.length > 0 && (
<div data-tag className="react-multi-email">
{/* // @TODO: user owners are appearing as guest here when should be only user input */}
{guestListEmails.map((email, index) => {
return (
<div key={index} className="cursor-pointer">
<span data-tag>{email}</span>
</div>
);
})}
</div>
)}
</div>
)}
</div>
@@ -523,13 +739,21 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
<textarea
{...bookingForm.register("notes")}
id="notes"
name="notes"
rows={3}
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-black dark:text-white dark:selection:bg-green-500 sm:text-sm"
className={classNames(
"focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm",
disableInput ? "bg-gray-200 dark:text-gray-500" : ""
)}
placeholder={t("share_additional_notes")}
disabled={disableInput}
/>
</div>
<div className="flex items-start space-x-2 rtl:space-x-reverse">
<Button type="submit" loading={mutation.isLoading}>
<Button
type="submit"
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
loading={mutation.isLoading}>
{rescheduleUid ? t("reschedule") : t("confirm")}
</Button>
<Button color="secondary" type="button" onClick={() => router.back()}>
@@ -547,7 +771,8 @@ const BookingPage = ({ eventType, booking, profile }: BookingPageProps) => {
</div>
<div className="ltr:ml-3 rtl:mr-3">
<p className="text-sm text-yellow-700">
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
{(mutation.error as HttpError)?.message}
</p>
</div>
</div>

View File

@@ -3,10 +3,10 @@ import { CheckIcon } from "@heroicons/react/solid";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import React, { PropsWithChildren, ReactNode } from "react";
import { useLocale } from "@lib/hooks/useLocale";
import { Button } from "@calcom/ui/Button";
import { DialogClose, DialogContent } from "@calcom/ui/Dialog";
import { DialogClose, DialogContent } from "@components/Dialog";
import { Button } from "@components/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
export type ConfirmationDialogContentProps = {
confirmBtn?: ReactNode;

View File

@@ -0,0 +1,97 @@
import { ClockIcon, XIcon } from "@heroicons/react/outline";
import { RescheduleResponse } from "pages/api/book/request-reschedule";
import React, { useState, Dispatch, SetStateAction } from "react";
import { useMutation } from "react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { TextArea } from "@calcom/ui/form/fields";
import * as fetchWrapper from "@lib/core/http/fetch-wrapper";
import { trpc } from "@lib/trpc";
interface IRescheduleDialog {
isOpenDialog: boolean;
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
bookingUId: string;
}
export const RescheduleDialog = (props: IRescheduleDialog) => {
const { t } = useLocale();
const utils = trpc.useContext();
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
const [rescheduleReason, setRescheduleReason] = useState("");
const [isLoading, setIsLoading] = useState(false);
const rescheduleApi = useMutation(
async () => {
setIsLoading(true);
try {
const result = await fetchWrapper.post<
{ bookingId: string; rescheduleReason: string },
RescheduleResponse
>("/api/book/request-reschedule", {
bookingId,
rescheduleReason,
});
if (result) {
showToast(t("reschedule_request_sent"), "success");
setIsOpenDialog(false);
}
} catch (error) {
showToast(t("unexpected_error_try_again"), "error");
// @TODO: notify sentry
}
setIsLoading(false);
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.bookings"]);
},
}
);
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<ClockIcon className="m-auto h-6 w-6"></ClockIcon>
</div>
<div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} />
<p className="-mt-8 text-sm text-gray-500">{t("reschedule_modal_description")}</p>
<p className="mt-6 mb-2 text-sm font-bold text-black">
{t("reason_for_reschedule_request")}
<span className="font-normal text-gray-500"> (Optional)</span>
</p>
<TextArea
data-testid="reschedule_reason"
name={t("reschedule_reason")}
value={rescheduleReason}
onChange={(e) => setRescheduleReason(e.target.value)}
className="mb-5 sm:mb-6"
/>
<DialogFooter>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<Button
data-testid="send_request"
disabled={isLoading}
onClick={() => {
rescheduleApi.mutate();
}}>
{t("send_reschedule_request")}
</Button>
</DialogFooter>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -2,34 +2,34 @@ import { ChevronDownIcon, PlusIcon } from "@heroicons/react/solid";
import { zodResolver } from "@hookform/resolvers/zod";
import { SchedulingType } from "@prisma/client";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import type { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
import { HttpError } from "@lib/core/http/error";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { slugify } from "@lib/slugify";
import { trpc } from "@lib/trpc";
import { Dialog, DialogClose, DialogContent } from "@components/Dialog";
import { Form, InputLeading, TextAreaField, TextField } from "@components/form/fields";
import { Alert } from "@components/ui/Alert";
import Avatar from "@components/ui/Avatar";
import { Button } from "@components/ui/Button";
import { Alert } from "@calcom/ui/Alert";
import { Button } from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@components/ui/Dropdown";
} from "@calcom/ui/Dropdown";
import { Form, InputLeading, TextAreaField, TextField } from "@calcom/ui/form/fields";
import { HttpError } from "@lib/core/http/error";
import { slugify } from "@lib/slugify";
import { trpc } from "@lib/trpc";
import Avatar from "@components/ui/Avatar";
import * as RadioArea from "@components/ui/form/radio-area";
// this describes the uniform data needed to create a new event type on Profile or Team
interface EventTypeParent {
export interface EventTypeParent {
teamId: number | null | undefined; // if undefined, then it's a profile
name?: string | null;
slug?: string | null;
@@ -56,13 +56,33 @@ export default function CreateEventTypeButton(props: Props) {
: undefined;
const pageSlug = router.query.eventPage || props.options[0].slug;
const hasTeams = !!props.options.find((option) => option.teamId);
const type: string = typeof router.query.type == "string" && router.query.type ? router.query.type : "";
const form = useForm<z.infer<typeof createEventTypeInput>>({
resolver: zodResolver(createEventTypeInput),
defaultValues: { length: 15 },
});
const { setValue, watch, register } = form;
useEffect(() => {
if (!router.isReady) return;
const title: string =
typeof router.query.title === "string" && router.query.title ? router.query.title : "";
const length: number =
typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15;
const description: string =
typeof router.query.description === "string" && router.query.description
? router.query.description
: "";
const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : "";
setValue("title", title);
setValue("length", length);
setValue("description", description);
setValue("slug", slug);
// If query params change, update the form
}, [router.isReady, router.query, setValue]);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
if (name === "title" && type === "change") {
@@ -75,7 +95,7 @@ export default function CreateEventTypeButton(props: Props) {
const createMutation = trpc.useMutation("viewer.eventTypes.create", {
onSuccess: async ({ eventType }) => {
await router.push("/event-types/" + eventType.id);
await router.replace("/event-types/" + eventType.id);
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
},
onError: (err) => {
@@ -113,7 +133,9 @@ export default function CreateEventTypeButton(props: Props) {
};
return (
<Dialog name="new-eventtype" clearQueryParamsOnClose={["eventPage", "teamId"]}>
<Dialog
name="new-eventtype"
clearQueryParamsOnClose={["eventPage", "teamId", "type", "description", "title", "length", "slug"]}>
{!hasTeams || props.isIndividualTeam ? (
<Button
onClick={() => openModal(props.options[0])}
@@ -178,7 +200,7 @@ export default function CreateEventTypeButton(props: Props) {
required
addOnLeading={
<InputLeading>
{process.env.NEXT_PUBLIC_APP_URL}/{pageSlug}/
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
</InputLeading>
}
{...register("slug")}
@@ -196,7 +218,6 @@ export default function CreateEventTypeButton(props: Props) {
required
min="10"
placeholder="15"
defaultValue={15}
label={t("length")}
className="pr-20"
{...register("length", { valueAsNumber: true })}
@@ -222,11 +243,17 @@ export default function CreateEventTypeButton(props: Props) {
{...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">
<RadioArea.Item value={SchedulingType.COLLECTIVE} className="w-1/2 text-sm">
<RadioArea.Item
value={SchedulingType.COLLECTIVE}
defaultChecked={type === SchedulingType.COLLECTIVE}
className="w-1/2 text-sm">
<strong className="mb-1 block">{t("collective")}</strong>
<p>{t("collective_description")}</p>
</RadioArea.Item>
<RadioArea.Item value={SchedulingType.ROUND_ROBIN} className="w-1/2 text-sm">
<RadioArea.Item
value={SchedulingType.ROUND_ROBIN}
defaultChecked={type === SchedulingType.ROUND_ROBIN}
className="w-1/2 text-sm">
<strong className="mb-1 block">{t("round_robin")}</strong>
<p>{t("round_robin_description")}</p>
</RadioArea.Item>

View File

@@ -1,11 +1,13 @@
import { ClockIcon, CreditCardIcon, UserIcon, UsersIcon } from "@heroicons/react/solid";
import { ClockIcon, CreditCardIcon, RefreshIcon, UserIcon, UsersIcon } from "@heroicons/react/solid";
import { SchedulingType } from "@prisma/client";
import { Prisma } from "@prisma/client";
import React from "react";
import React, { useMemo } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecurringEvent } from "@calcom/types/Calendar";
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
select: {
@@ -14,6 +16,7 @@ const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
price: true,
currency: true,
schedulingType: true,
recurringEvent: true,
description: true,
},
});
@@ -28,6 +31,11 @@ export type EventTypeDescriptionProps = {
export const EventTypeDescription = ({ eventType, className }: EventTypeDescriptionProps) => {
const { t } = useLocale();
const recurringEvent: RecurringEvent = useMemo(
() => (eventType.recurringEvent as RecurringEvent) || [],
[eventType.recurringEvent]
);
return (
<>
<div className={classNames("text-neutral-500 dark:text-white", className)}>
@@ -54,6 +62,12 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
{t("1_on_1")}
</li>
)}
{recurringEvent?.count && recurringEvent.count > 0 && (
<li className="flex whitespace-nowrap">
<RefreshIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
{t("repeats_up_to", { count: recurringEvent.count })}
</li>
)}
{eventType.price > 0 && (
<li className="flex whitespace-nowrap">
<CreditCardIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />

View File

@@ -0,0 +1,132 @@
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import React, { useState } from "react";
import { UseFormReturn } from "react-hook-form";
import { Frequency as RRuleFrequency } from "rrule";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecurringEvent } from "@calcom/types/Calendar";
import Select from "@components/ui/form/Select";
type RecurringEventControllerProps = { recurringEvent: RecurringEvent; formMethods: UseFormReturn<any, any> };
export default function RecurringEventController({
recurringEvent,
formMethods,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventDefined, setRecurringEventDefined] = useState(recurringEvent?.count !== undefined);
const [recurringEventInterval, setRecurringEventInterval] = useState(recurringEvent?.interval || 1);
const [recurringEventFrequency, setRecurringEventFrequency] = useState(
recurringEvent?.freq || RRuleFrequency.WEEKLY
);
const [recurringEventCount, setRecurringEventCount] = useState(recurringEvent?.count || 12);
/* Just yearly-0, monthly-1 and weekly-2 */
const recurringEventFreqOptions = Object.entries(RRuleFrequency)
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
.map(([key, value]) => ({
label: t(`${key.toString().toLowerCase()}`, { count: recurringEventInterval }),
value: value.toString(),
}));
return (
<div className="block items-start sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor="recurringEvent" className="flex text-sm font-medium text-neutral-700">
{t("recurring_event")}
</label>
</div>
<div className="w-full">
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
onChange={(event) => {
setRecurringEventDefined(event?.target.checked);
if (!event?.target.checked) {
formMethods.setValue("recurringEvent", {});
} else {
formMethods.setValue(
"recurringEvent",
recurringEventDefined
? recurringEvent
: {
interval: 1,
count: 12,
freq: RRuleFrequency.WEEKLY,
}
);
}
recurringEvent = formMethods.getValues("recurringEvent");
}}
type="checkbox"
className="text-primary-600 h-4 w-4 rounded border-gray-300"
defaultChecked={recurringEventDefined}
data-testid="recurring-event-check"
/>
</div>
<div className="text-sm ltr:ml-3 rtl:mr-3">
<p className="text-neutral-900">{t("recurring_event_description")}</p>
</div>
</div>
<Collapsible
open={recurringEventDefined}
data-testid="recurring-event-collapsible"
onOpenChange={() => setRecurringEventDefined(!recurringEventDefined)}>
<CollapsibleContent className="mt-4 text-sm">
<div className="flex items-center">
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
<input
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"
defaultValue={recurringEvent?.interval || 1}
onChange={(event) => {
setRecurringEventInterval(parseInt(event?.target.value));
recurringEvent.interval = parseInt(event?.target.value);
formMethods.setValue("recurringEvent", recurringEvent);
}}
/>
<Select
options={recurringEventFreqOptions}
value={recurringEventFreqOptions[recurringEventFrequency]}
isSearchable={false}
className="w-18 block min-w-0 rounded-sm sm:text-sm"
onChange={(e) => {
if (e?.value) {
setRecurringEventFrequency(parseInt(e?.value));
recurringEvent.freq = parseInt(e?.value);
formMethods.setValue("recurringEvent", recurringEvent);
}
}}
/>
</div>
<div className="mt-4 flex items-center">
<p className="mr-2 text-neutral-900">{t("max")}</p>
<input
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"
defaultValue={recurringEvent?.count || 12}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
recurringEvent.count = parseInt(event?.target.value);
formMethods.setValue("recurringEvent", recurringEvent);
}}
/>
<p className="mr-2 text-neutral-900">
{t(`${RRuleFrequency[recurringEventFrequency].toString().toLowerCase()}`, {
count: recurringEventCount,
})}
</p>
</div>
</CollapsibleContent>
</Collapsible>
</div>
</div>
);
}

View File

@@ -0,0 +1,63 @@
import { LinkIcon } from "@heroicons/react/outline";
import { ClockIcon, DotsHorizontalIcon, ExternalLinkIcon, UserIcon } from "@heroicons/react/solid";
import React from "react";
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
function SkeletonLoader() {
return (
<SkeletonContainer>
<div className="mb-4 flex items-center">
<SkeletonAvatar width="8" height="8"></SkeletonAvatar>
<div className="space-y-1">
<SkeletonText height="4" width="16"></SkeletonText>
<SkeletonText height="4" width="24"></SkeletonText>
</div>
</div>
<ul className="divide-y divide-neutral-200 border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</ul>
</SkeletonContainer>
);
}
export default SkeletonLoader;
function SkeletonItem() {
return (
<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>
</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>
</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>
</li>
</ul>
</div>
</div>
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
<div className="flex justify-between rtl:space-x-reverse">
<div className="btn-icon appearance-none">
<ExternalLinkIcon className="h-5 w-5" />
</div>
<div className="btn-icon appearance-none">
<LinkIcon className="h-5 w-5" />
</div>
<div className="btn-icon appearance-none">
<DotsHorizontalIcon className="h-5 w-5" />
</div>
</div>
</div>
</li>
);
}

View File

@@ -1,20 +1,20 @@
import React, { Fragment } from "react";
import { Fragment } from "react";
import { useMutation } from "react-query";
import { InstallAppButton } from "@calcom/app-store/components";
import showToast from "@calcom/lib/notification";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import Switch from "@calcom/ui/Switch";
import { QueryCell } from "@lib/QueryCell";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { trpc } from "@lib/trpc";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
import { List } from "@components/List";
import { ShellSubHeading } from "@components/Shell";
import { Alert } from "@components/ui/Alert";
import Button from "@components/ui/Button";
import ConnectIntegration from "./ConnectIntegrations";
import DisconnectIntegration from "./DisconnectIntegration";
import IntegrationListItem from "./IntegrationListItem";
import SubHeadingTitleWithConnections from "./SubHeadingTitleWithConnections";
@@ -110,7 +110,8 @@ function ConnectedCalendarsList(props: Props) {
<Fragment key={item.credentialId}>
{item.calendars ? (
<IntegrationListItem
{...item.integration}
title={item.integration.title}
imageSrc={item.integration.imageSrc}
description={item.primary?.externalId || "No external Id"}
actions={
<DisconnectIntegration
@@ -127,8 +128,8 @@ function ConnectedCalendarsList(props: Props) {
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
externalId={cal.externalId as string}
title={cal.name as string}
externalId={cal.externalId}
title={cal.name || "Nameless calendar"}
type={item.integration.type}
defaultSelected={cal.isSelected}
/>
@@ -138,7 +139,7 @@ function ConnectedCalendarsList(props: Props) {
) : (
<Alert
severity="warning"
title="Something went wrong"
title={t("calendar_error")}
message={item.error?.message}
actions={
<DisconnectIntegration
@@ -174,16 +175,18 @@ function CalendarList(props: Props) {
{data.calendar.items.map((item) => (
<IntegrationListItem
key={item.title}
{...item}
title={item.title}
imageSrc={item.imageSrc}
description={item.description}
actions={
<ConnectIntegration
<InstallAppButton
type={item.type}
render={(btnProps) => (
<Button color="secondary" {...btnProps} data-testid="integration-connection-button">
render={(buttonProps) => (
<Button color="secondary" {...buttonProps}>
{t("connect")}
</Button>
)}
onOpenChange={() => props.onChanged()}
onChanged={() => props.onChanged()}
/>
}
/>
@@ -193,6 +196,7 @@ function CalendarList(props: Props) {
/>
);
}
export function CalendarListContainer(props: { heading?: false }) {
const { t } = useLocale();
const { heading = true } = props;

View File

@@ -1,64 +0,0 @@
import type { IntegrationOAuthCallbackState } from "pages/api/integrations/types";
import { useState } from "react";
import { useMutation } from "react-query";
import { NEXT_PUBLIC_BASE_URL } from "@lib/config/constants";
import { AddAppleIntegrationModal } from "@lib/integrations/calendar/components/AddAppleIntegration";
import { AddCalDavIntegrationModal } from "@lib/integrations/calendar/components/AddCalDavIntegration";
import { ButtonBaseProps } from "@components/ui/Button";
export default function ConnectIntegration(props: {
type: string;
render: (renderProps: ButtonBaseProps) => JSX.Element;
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
}) {
const { type } = props;
const [isLoading, setIsLoading] = useState(false);
const mutation = useMutation(async () => {
const state: IntegrationOAuthCallbackState = {
returnTo: NEXT_PUBLIC_BASE_URL + location.pathname + location.search,
};
const stateStr = encodeURIComponent(JSON.stringify(state));
const searchParams = `?state=${stateStr}`;
const res = await fetch("/api/integrations/" + type.replace("_", "") + "/add" + searchParams);
if (!res.ok) {
throw new Error("Something went wrong");
}
const json = await res.json();
window.location.href = json.url;
setIsLoading(true);
});
const [isModalOpen, _setIsModalOpen] = useState(false);
const setIsModalOpen = (v: boolean) => {
_setIsModalOpen(v);
props.onOpenChange(v);
};
return (
<>
{props.render({
onClick() {
if (["caldav_calendar", "apple_calendar"].includes(type)) {
// special handlers
setIsModalOpen(true);
return;
}
mutation.mutate();
},
loading: mutation.isLoading || isLoading,
disabled: isModalOpen,
})}
{type === "caldav_calendar" && (
<AddCalDavIntegrationModal open={isModalOpen} onOpenChange={setIsModalOpen} />
)}
{type === "apple_calendar" && (
<AddAppleIntegrationModal open={isModalOpen} onOpenChange={setIsModalOpen} />
)}
</>
);
}

View File

@@ -1,9 +1,11 @@
import { useState } from "react";
import { useMutation } from "react-query";
import { Dialog } from "@components/Dialog";
import showToast from "@calcom/lib/notification";
import { ButtonBaseProps } from "@calcom/ui/Button";
import { Dialog } from "@calcom/ui/Dialog";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import { ButtonBaseProps } from "@components/ui/Button";
export default function DisconnectIntegration(props: {
/** Integration credential id */
@@ -24,12 +26,14 @@ export default function DisconnectIntegration(props: {
if (!res.ok) {
throw new Error("Something went wrong");
}
return res.json();
},
{
async onSettled() {
await props.onOpenChange(modalOpen);
},
onSuccess() {
onSuccess(data) {
showToast(data.message, "success");
setModalOpen(false);
},
}

View File

@@ -1,4 +1,3 @@
import Image from "next/image";
import { ReactNode } from "react";
import classNames from "@lib/classNames";
@@ -6,7 +5,7 @@ import classNames from "@lib/classNames";
import { ListItem, ListItemText, ListItemTitle } from "@components/List";
function IntegrationListItem(props: {
imageSrc: string;
imageSrc?: string;
title: string;
description: string;
actions?: ReactNode;
@@ -15,7 +14,7 @@ function IntegrationListItem(props: {
return (
<ListItem expanded={!!props.children} className={classNames("flex-col")}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
<Image width={40} height={40} src={`/${props.imageSrc}`} alt={props.title} />
{props.imageSrc && <img className="h-10 w-10" src={props.imageSrc} alt={props.title} />}
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">{props.title}</ListItemTitle>
<ListItemText component="p">{props.description}</ListItemText>

View File

@@ -1,11 +1,12 @@
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
import React, { FC } from "react";
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
import Select from "react-select";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import Button from "@components/ui/Button";
import Select from "@components/ui/form/Select";
interface OptionTypeBase {
label: string;
@@ -55,7 +56,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
defaultValue={selectedInputOption}
options={inputOptions}
isSearchable={false}
className="focus:border-primary-500 focus:ring-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
className="mt-1 mb-2 block w-full min-w-0 flex-1 sm:text-sm"
onChange={(option) => option && field.onChange(option.value)}
value={selectedInputOption}
onBlur={field.onBlur}
@@ -73,7 +74,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
type="text"
id="label"
required
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 text-sm shadow-sm"
className="block w-full rounded-sm border-gray-300 text-sm shadow-sm"
defaultValue={selectedCustomInput?.label}
{...register("label", { required: true })}
/>
@@ -89,7 +90,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
<input
type="text"
id="placeholder"
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 text-sm shadow-sm"
className="block w-full rounded-sm border-gray-300 text-sm shadow-sm"
defaultValue={selectedCustomInput?.placeholder}
{...register("placeholder")}
/>

View File

@@ -1,10 +1,10 @@
import React, { SyntheticEvent, useState } from "react";
import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button";
import { ErrorCode } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import Button from "@components/ui/Button";
const ChangePasswordSection = () => {
const [oldPassword, setOldPassword] = useState("");
@@ -56,13 +56,13 @@ const ChangePasswordSection = () => {
return (
<>
<div className="mt-6">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
</div>
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
<div className="py-6 lg:pb-8">
<div className="flex">
<div className="w-1/2 ltr:mr-2 rtl:ml-2">
<div className="py-6 lg:pb-5">
<div className="my-3">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
</div>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0">
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-1/2">
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
{t("current_password")}
</label>
@@ -74,12 +74,12 @@ const ChangePasswordSection = () => {
name="current_password"
id="current_password"
required
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black sm:text-sm"
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
placeholder={t("your_old_password")}
/>
</div>
</div>
<div className="ml-2 w-1/2">
<div className="w-full sm:w-1/2">
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
{t("new_password")}
</label>
@@ -91,17 +91,18 @@ const ChangePasswordSection = () => {
value={newPassword}
required
onInput={(e) => setNewPassword(e.currentTarget.value)}
className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black sm:text-sm"
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
placeholder={t("super_secure_new_password")}
/>
</div>
</div>
</div>
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
<div className="flex justify-end py-8">
<Button type="submit">{t("save")}</Button>
<div className="flex py-8 sm:justify-end">
<Button color="secondary" type="submit">
{t("save")}
</Button>
</div>
<hr className="mt-4" />
</div>
</form>
</>

View File

@@ -1,11 +1,11 @@
import { SyntheticEvent, useState } from "react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { ErrorCode } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import { Dialog, DialogContent } from "@components/Dialog";
import Button from "@components/ui/Button";
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
import TwoFactorModalHeader from "./TwoFactorModalHeader";
@@ -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 focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm"
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
/>
</div>

View File

@@ -1,11 +1,11 @@
import React, { SyntheticEvent, useState } from "react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { ErrorCode } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import { Dialog, DialogContent } from "@components/Dialog";
import Button from "@components/ui/Button";
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
import TwoFactorModalHeader from "./TwoFactorModalHeader";
@@ -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 focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm"
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
/>
</div>
@@ -172,7 +172,8 @@ 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 focus:border-neutral-900 focus:ring-neutral-900 sm:text-sm"
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
autoComplete="one-time-code"
/>
</div>

View File

@@ -1,9 +1,10 @@
import { useState } from "react";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import Badge from "@components/ui/Badge";
import Button from "@components/ui/Button";
import DisableTwoFactorModal from "./DisableTwoFactorModal";
import EnableTwoFactorModal from "./EnableTwoFactorModal";
@@ -16,21 +17,25 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
return (
<>
<div className="flex flex-row items-center">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("2fa")}</h2>
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
{enabled ? t("enabled") : t("disabled")}
</Badge>
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("2fa")}</h2>
<Badge className="ml-2 text-xs" variant={enabled ? "success" : "gray"}>
{enabled ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Button
type="submit"
color="secondary"
onClick={() => (enabled ? setDisableModalOpen(true) : setEnableModalOpen(true))}>
{enabled ? t("disable") : t("enable")}
</Button>
</div>
</div>
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
<Button
className="mt-6"
type="submit"
onClick={() => (enabled ? setDisableModalOpen(true) : setEnableModalOpen(true))}>
{enabled ? t("disable") : t("enable")} {t("2fa")}
</Button>
{enableModalOpen && (
<EnableTwoFactorModal
onEnable={() => {

View File

@@ -67,14 +67,14 @@ const constructImage = (name: string, description: string, username: string): st
return (
encodeURIComponent("Meet **" + name + "** <br>" + description).replace(/'/g, "%27") +
".png?md=1&images=https%3A%2F%2Fcal.com%2Flogo-white.svg&images=" +
(process.env.NEXT_PUBLIC_APP_URL || process.env.BASE_URL) +
(process.env.NEXT_PUBLIC_WEBSITE_URL || process.env.NEXT_PUBLIC_WEBAPP_URL) +
"/" +
username +
"/avatar.png"
);
};
export const HeadSeo: React.FC<HeadSeoProps & { children?: never }> = (props) => {
export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
const defaultUrl = getBrowserInfo()?.url;
const image = getSeoImage("default");
@@ -113,3 +113,5 @@ export const HeadSeo: React.FC<HeadSeoProps & { children?: never }> = (props) =>
return <NextSeo {...seoProps} />;
};
export default HeadSeo;

View File

@@ -1,20 +1,40 @@
import { MembershipRole } from "@prisma/client";
import { useState } from "react";
import React, { SyntheticEvent } from "react";
import React, { SyntheticEvent, useEffect } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
import Button from "@components/ui/Button";
import ModalContainer from "@components/ui/ModalContainer";
import Select from "@components/ui/form/Select";
type MembershipRoleOption = {
value: MembershipRole;
label?: string;
};
const options: MembershipRoleOption[] = [{ value: "MEMBER" }, { value: "ADMIN" }, { value: "OWNER" }];
export default function MemberChangeRoleModal(props: {
isOpen: boolean;
currentMember: MembershipRole;
memberId: number;
teamId: number;
initialRole: MembershipRole;
onExit: () => void;
}) {
const [role, setRole] = useState(props.initialRole || MembershipRole.MEMBER);
useEffect(() => {
options.forEach((option, i) => {
options[i].label = t(option.value.toLowerCase());
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [role, setRole] = useState(
options.find((option) => option.value === props.initialRole || MembershipRole.MEMBER)!
);
const [errorMessage, setErrorMessage] = useState("");
const { t } = useLocale();
const utils = trpc.useContext();
@@ -35,12 +55,11 @@ export default function MemberChangeRoleModal(props: {
changeRoleMutation.mutate({
teamId: props.teamId,
memberId: props.memberId,
role,
role: role.value,
});
}
return (
<ModalContainer>
<ModalContainer isOpen={props.isOpen} onExit={props.onExit}>
<>
<div className="mb-4 sm:flex sm:items-start">
<div className="text-center sm:text-left">
@@ -54,17 +73,16 @@ export default function MemberChangeRoleModal(props: {
<label className="mb-2 block text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
{t("role")}
</label>
<select
{/*<option value="OWNER">{t("owner")}</option> - needs dialog to confirm change of ownership */}
<Select
isSearchable={false}
options={props.currentMember !== MembershipRole.OWNER ? options.slice(0, 2) : options}
value={role}
onChange={(e) => setRole(e.target.value as MembershipRole)}
onChange={(option) => option && setRole(option)}
id="role"
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm">
<option value="MEMBER">{t("member")}</option>
<option value="ADMIN">{t("admin")}</option>
{/*<option value="OWNER">{t("owner")}</option> - needs dialog to confirm change of ownership */}
</select>
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm sm:text-sm"
/>
</div>
{errorMessage && (
<p className="text-sm text-red-700">
<span className="font-bold">Error: </span>

View File

@@ -1,21 +1,44 @@
import { UserIcon } from "@heroicons/react/outline";
import { InformationCircleIcon } from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client";
import { useState } from "react";
import React, { SyntheticEvent } from "react";
import React, { useState, useEffect, SyntheticEvent, useMemo } from "react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { TextField } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
import { TeamWithMembers } from "@lib/queries/teams";
import { trpc } from "@lib/trpc";
import { TextField } from "@components/form/fields";
import Button from "@components/ui/Button";
import Select from "@components/ui/form/Select";
export default function MemberInvitationModal(props: { team: TeamWithMembers | null; onExit: () => void }) {
type MemberInvitationModalProps = {
isOpen: boolean;
team: TeamWithMembers | null;
currentMember: MembershipRole;
onExit: () => void;
};
type MembershipRoleOption = {
value: MembershipRole;
label?: string;
};
const _options: MembershipRoleOption[] = [{ value: "MEMBER" }, { value: "ADMIN" }, { value: "OWNER" }];
export default function MemberInvitationModal(props: MemberInvitationModalProps) {
const [errorMessage, setErrorMessage] = useState("");
const { t, i18n } = useLocale();
const utils = trpc.useContext();
const options = useMemo(() => {
_options.forEach((option, i) => {
_options[i].label = t(option.value.toLowerCase());
});
return _options;
}, [t]);
const inviteMemberMutation = trpc.useMutation("viewer.teams.inviteMember", {
async onSuccess() {
await utils.invalidateQueries(["viewer.teams.get"]);
@@ -48,102 +71,89 @@ export default function MemberInvitationModal(props: { team: TeamWithMembers | n
}
return (
<div
className="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 z-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<div className="inline-block transform rounded-lg bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<UserIcon className="text-brandcontrast h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("invite_new_member")}
</h3>
<div>
<p className="text-sm text-gray-400">{t("invite_new_team_member")}</p>
</div>
<Dialog open={props.isOpen} onOpenChange={props.onExit}>
<DialogContent>
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<UserIcon className="text-brandcontrast h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("invite_new_member")}
</h3>
<div>
<p className="text-sm text-gray-400">{t("invite_new_team_member")}</p>
</div>
</div>
<form onSubmit={inviteMember}>
<div className="space-y-4">
<TextField
label={t("email_or_username")}
id="inviteUser"
name="inviteUser"
placeholder="email@example.com"
required
/>
<div>
<label className="mb-1 block text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
{t("role")}
</label>
<select
id="role"
className="focus:border-brand mt-1 block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black sm:text-sm">
<option value="MEMBER">{t("member")}</option>
<option value="ADMIN">{t("admin")}</option>
</select>
</div>
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
type="checkbox"
name="sendInviteEmail"
defaultChecked
id="sendInviteEmail"
className="focus:border-brand rounded-sm border-gray-300 text-black shadow-sm focus:ring-black sm:text-sm"
/>
</div>
<div className="text-sm ltr:ml-2 rtl:mr-2">
<label htmlFor="sendInviteEmail" className="font-medium text-gray-700">
{t("send_invite_email")}
</label>
</div>
</div>
<div className="flex flex-row rounded-sm bg-gray-50 px-3 py-2">
<InformationCircleIcon className="h-5 w-5 flex-shrink-0 fill-gray-400" aria-hidden="true" />
<span className="ml-2 text-sm leading-tight text-gray-500">
Note: This will cost an extra seat ($12/m) on your subscription if this invitee does not
have a pro account.{" "}
<a href="#" className="underline">
Learn More
</a>
</span>
</div>
</div>
{errorMessage && (
<p className="text-sm text-red-700">
<span className="font-bold">Error: </span>
{errorMessage}
</p>
)}
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button
type="submit"
color="primary"
className="ltr:ml-2 rtl:mr-2"
data-testid="invite-new-member-button">
{t("invite")}
</Button>
<Button type="button" color="secondary" onClick={props.onExit}>
{t("cancel")}
</Button>
</div>
</form>
</div>
</div>
</div>
<form onSubmit={inviteMember}>
<div className="space-y-4">
<TextField
label={t("email_or_username")}
id="inviteUser"
name="inviteUser"
placeholder="email@example.com"
required
/>
<div>
<label className="mb-1 block text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
{t("role")}
</label>
<Select
defaultValue={options[0]}
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"
/>
</div>
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
type="checkbox"
name="sendInviteEmail"
defaultChecked
id="sendInviteEmail"
className="rounded-sm border-gray-300 text-black shadow-sm sm:text-sm"
/>
</div>
<div className="text-sm ltr:ml-2 rtl:mr-2">
<label htmlFor="sendInviteEmail" className="font-medium text-gray-700">
{t("send_invite_email")}
</label>
</div>
</div>
<div className="flex flex-row rounded-sm bg-gray-50 px-3 py-2">
<InformationCircleIcon className="h-5 w-5 flex-shrink-0 fill-gray-400" aria-hidden="true" />
<span className="ml-2 text-sm leading-tight text-gray-500">
Note: This will cost an extra seat ($12/m) on your subscription if this invitee does not have
a pro account.{" "}
<a href="#" className="underline">
Learn More
</a>
</span>
</div>
</div>
{errorMessage && (
<p className="text-sm text-red-700">
<span className="font-bold">Error: </span>
{errorMessage}
</p>
)}
<DialogFooter>
<Button type="button" color="secondary" onClick={props.onExit}>
{t("cancel")}
</Button>
<Button
type="submit"
color="primary"
className="ltr:ml-2 rtl:mr-2"
data-testid="invite-new-member-button">
{t("invite")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,31 +1,32 @@
import { UserRemoveIcon, PencilIcon } from "@heroicons/react/outline";
import { ClockIcon, ExternalLinkIcon, DotsHorizontalIcon } from "@heroicons/react/solid";
import { PencilIcon, UserRemoveIcon } from "@heroicons/react/outline";
import { ClockIcon, DotsHorizontalIcon, ExternalLinkIcon } from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client";
import Link from "next/link";
import React, { useState } from "react";
import TeamAvailabilityModal from "@ee/components/team/availability/TeamAvailabilityModal";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { trpc, inferQueryOutput } from "@lib/trpc";
import { Dialog, DialogTrigger } from "@components/Dialog";
import { Tooltip } from "@components/Tooltip";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";
import Button from "@components/ui/Button";
import ModalContainer from "@components/ui/ModalContainer";
import { useState } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/Dropdown";
} from "@calcom/ui/Dropdown";
import { Tooltip } from "@calcom/ui/Tooltip";
import TeamAvailabilityModal from "@ee/components/team/availability/TeamAvailabilityModal";
import useCurrentUserId from "@lib/hooks/useCurrentUserId";
import { inferQueryOutput, trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";
import ModalContainer from "@components/ui/ModalContainer";
import MemberChangeRoleModal from "./MemberChangeRoleModal";
import TeamPill, { TeamRole } from "./TeamPill";
import { MembershipRole } from ".prisma/client";
interface Props {
team: inferQueryOutput<"viewer.teams.get">;
@@ -49,6 +50,14 @@ export default function MemberListItem(props: Props) {
},
});
const ownersInTeam = () => {
const { members } = props.team;
const owners = members.filter((member) => member["role"] === MembershipRole.OWNER && member["accepted"]);
return owners.length;
};
const currentUserId = useCurrentUserId();
const name =
props.member.name ||
(() => {
@@ -65,7 +74,7 @@ export default function MemberListItem(props: Props) {
<div className="flex w-full flex-col justify-between sm:flex-row">
<div className="flex">
<Avatar
imageSrc={getPlaceholderAvatar(props.member?.avatar, name)}
imageSrc={WEBSITE_URL + "/" + props.member.username + "/avatar.png"}
alt={name || ""}
className="h-9 w-9 rounded-full"
/>
@@ -121,8 +130,12 @@ export default function MemberListItem(props: Props) {
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
{(props.team.membership.role === MembershipRole.OWNER ||
props.team.membership.role === MembershipRole.ADMIN) && (
{((props.team.membership.role === MembershipRole.OWNER &&
(props.member.role !== MembershipRole.OWNER ||
ownersInTeam() > 1 ||
props.member.id !== currentUserId)) ||
(props.team.membership.role === MembershipRole.ADMIN &&
props.member.role !== MembershipRole.OWNER)) && (
<>
<DropdownMenuItem>
<Button
@@ -164,6 +177,8 @@ export default function MemberListItem(props: Props) {
</div>
{showChangeMemberRoleModal && (
<MemberChangeRoleModal
isOpen={showChangeMemberRoleModal}
currentMember={props.team.membership.role}
teamId={props.team?.id}
memberId={props.member.id}
initialRole={props.member.role as MembershipRole}
@@ -171,12 +186,16 @@ export default function MemberListItem(props: Props) {
/>
)}
{showTeamAvailabilityModal && (
<ModalContainer wide noPadding>
<ModalContainer
wide
noPadding
isOpen={showTeamAvailabilityModal}
onExit={() => setShowTeamAvailabilityModal(false)}>
<TeamAvailabilityModal team={props.team} member={props.member} />
<div className="space-x-2 border-t p-5 rtl:space-x-reverse">
<div className="space-x-2 border-t py-5 rtl:space-x-reverse">
<Button onClick={() => setShowTeamAvailabilityModal(false)}>{t("done")}</Button>
{props.team.membership.role !== MembershipRole.MEMBER && (
<Link href={`/settings/teams/${props.team.id}/availability`}>
<Link href={`/settings/teams/${props.team.id}/availability`} passHref>
<Button color="secondary">{t("Open Team Availability")}</Button>
</Link>
)}

View File

@@ -1,12 +1,15 @@
import { UsersIcon } from "@heroicons/react/outline";
import { useRef, useState } from "react";
import { useLocale } from "@lib/hooks/useLocale";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert";
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
import { trpc } from "@lib/trpc";
import { Alert } from "@components/ui/Alert";
interface Props {
isOpen: boolean;
onClose: () => void;
}
@@ -32,24 +35,12 @@ export default function TeamCreate(props: Props) {
};
return (
<div
className="fixed inset-0 z-50 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 z-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
<>
<Dialog open={props.isOpen} onOpenChange={props.onClose}>
<DialogContent>
<div className="mb-4 sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-neutral-100 sm:mx-0 sm:h-10 sm:w-10">
<UsersIcon className="h-6 w-6 text-neutral-900" />
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<UsersIcon className="text-brandcontrast h-6 w-6" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
@@ -72,21 +63,25 @@ 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 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
/>
</div>
{errorMessage && <Alert severity="error" title={errorMessage} />}
<div className="mt-5 flex flex-row-reverse sm:mt-4">
<button type="submit" className="btn btn-primary">
{t("create_team")}
</button>
<button onClick={props.onClose} type="button" className="btn btn-white ltr:mr-2">
<DialogFooter>
<Button type="button" color="secondary" onClick={props.onClose}>
{t("cancel")}
</button>
</div>
</Button>
<Button
type="submit"
color="primary"
className="ltr:ml-2 rtl:mr-2"
data-testid="create-new-team-button">
{t("create_team")}
</Button>
</DialogFooter>
</form>
</div>
</div>
</div>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -1,4 +1,5 @@
import showToast from "@lib/notification";
import showToast from "@calcom/lib/notification";
import { trpc, inferQueryOutput } from "@lib/trpc";
import TeamListItem from "./TeamListItem";

View File

@@ -1,27 +1,34 @@
import { ExternalLinkIcon, TrashIcon, LogoutIcon, PencilIcon } from "@heroicons/react/outline";
import { LinkIcon, DotsHorizontalIcon } from "@heroicons/react/solid";
import { LogoutIcon } from "@heroicons/react/outline";
import {
ExternalLinkIcon,
TrashIcon,
LinkIcon,
DotsHorizontalIcon,
PencilIcon,
} from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client";
import Link from "next/link";
import classNames from "@lib/classNames";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { trpc, inferQueryOutput } from "@lib/trpc";
import { Dialog, DialogTrigger } from "@components/Dialog";
import { Tooltip } from "@components/Tooltip";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";
import Button from "@components/ui/Button";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@components/ui/Dropdown";
} from "@calcom/ui/Dropdown";
import { Tooltip } from "@calcom/ui/Tooltip";
import classNames from "@lib/classNames";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import { trpc, inferQueryOutput } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";
import { TeamRole } from "./TeamPill";
import { MembershipRole } from ".prisma/client";
interface Props {
team: inferQueryOutput<"viewer.teams.list">[number];
@@ -65,7 +72,7 @@ export default function TeamListItem(props: Props) {
<div className="ml-3 inline-block">
<span className="text-sm font-bold text-neutral-700">{team.name}</span>
<span className="block text-xs text-gray-400">
{process.env.NEXT_PUBLIC_APP_URL}/team/{team.slug}
{process.env.NEXT_PUBLIC_WEBSITE_URL}/team/{team.slug}
</span>
</div>
</div>
@@ -105,7 +112,7 @@ export default function TeamListItem(props: Props) {
<Tooltip content={t("copy_link_team")}>
<Button
onClick={() => {
navigator.clipboard.writeText(process.env.NEXT_PUBLIC_APP_URL + "/team/" + team.slug);
navigator.clipboard.writeText(process.env.NEXT_PUBLIC_WEBSITE_URL + "/team/" + team.slug);
showToast(t("link_copied"), "success");
}}
className="h-10 w-10 transition-none"
@@ -125,9 +132,9 @@ export default function TeamListItem(props: Props) {
<Link href={"/settings/teams/" + team.id}>
<a>
<Button
type="button"
color="minimal"
className="w-full font-normal"
size="sm"
className="w-full rounded-none font-medium"
StartIcon={PencilIcon}>
{t("edit_team")}
</Button>
@@ -135,16 +142,14 @@ export default function TeamListItem(props: Props) {
</Link>
</DropdownMenuItem>
)}
{isAdmin && <DropdownMenuSeparator className="h-px bg-gray-200" />}
<DropdownMenuItem>
<Link href={`${process.env.NEXT_PUBLIC_APP_URL}/team/${team.slug}`} passHref={true}>
<Link href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${team.slug}`} passHref={true}>
<a target="_blank">
<Button
type="button"
color="minimal"
className="w-full font-normal"
size="sm"
className="w-full rounded-none font-medium"
StartIcon={ExternalLinkIcon}>
{" "}
{t("preview_team")}
</Button>
</a>
@@ -160,8 +165,9 @@ export default function TeamListItem(props: Props) {
e.stopPropagation();
}}
color="warn"
StartIcon={TrashIcon}
className="w-full font-normal">
size="sm"
className="w-full rounded-none font-medium"
StartIcon={TrashIcon}>
{t("disband_team")}
</Button>
</DialogTrigger>
@@ -183,8 +189,9 @@ export default function TeamListItem(props: Props) {
<Button
type="button"
color="warn"
size="lg"
StartIcon={LogoutIcon}
className="w-full"
className="w-full rounded-none"
onClick={(e) => {
e.stopPropagation();
}}>

View File

@@ -1,15 +1,17 @@
import { HashtagIcon, InformationCircleIcon, LinkIcon, PhotographIcon } from "@heroicons/react/solid";
import React, { useRef, useState } from "react";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { objectKeys } from "@calcom/lib/objectKeys";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { TextField } from "@calcom/ui/form/fields";
import { TeamWithMembers } from "@lib/queries/teams";
import { trpc } from "@lib/trpc";
import ImageUploader from "@components/ImageUploader";
import { TextField } from "@components/form/fields";
import { Alert } from "@components/ui/Alert";
import Button from "@components/ui/Button";
import SettingInputContainer from "@components/ui/SettingInputContainer";
interface Props {
@@ -53,9 +55,9 @@ export default function TeamSettings(props: Props) {
hideBranding: hideBrandingRef.current?.checked,
};
// remove unchanged variables
for (const key in variables) {
if (variables[key] === team?.[key]) delete variables[key];
}
objectKeys(variables).forEach((key) => {
if (variables[key as keyof typeof variables] === team?.[key]) delete variables[key];
});
mutation.mutate({ id: team.id, ...variables });
}
@@ -90,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_APP_URL}/{"team/"}
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{"team/"}
</span>
}
ref={teamUrlRef}
@@ -110,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 focus:border-neutral-800 focus:outline-none focus:ring-neutral-800 sm:text-sm"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
defaultValue={team?.name as string}
/>
}
@@ -129,7 +131,7 @@ 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 focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"></textarea>
className="mt-1 block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"></textarea>
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
</>
}
@@ -149,7 +151,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 focus:border-neutral-800 focus:outline-none focus:ring-neutral-800 sm:text-sm"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 shadow-sm sm:text-sm"
defaultValue={team?.logo ?? undefined}
/>
<ImageUploader

View File

@@ -1,26 +1,26 @@
import { ClockIcon, ExternalLinkIcon, LinkIcon, LogoutIcon, TrashIcon } from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client";
import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
import showToast from "@calcom/lib/notification";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { TeamWithMembers } from "@lib/queries/teams";
import { trpc } from "@lib/trpc";
import { Dialog, DialogTrigger } from "@components/Dialog";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
import LinkIconButton from "@components/ui/LinkIconButton";
import { MembershipRole } from ".prisma/client";
export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers; role: MembershipRole }) {
const { t } = useLocale();
const utils = trpc.useContext();
const router = useRouter();
const permalink = `${process.env.NEXT_PUBLIC_APP_URL}/team/${props.team?.slug}`;
const permalink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${props.team?.slug}`;
const deleteTeamMutation = trpc.useMutation("viewer.teams.delete", {
async onSuccess() {

View File

@@ -1,9 +1,8 @@
import { useState } from "react";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { trpc } from "@lib/trpc";
import showToast from "@calcom/lib/notification";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import {
Dialog,
DialogTrigger,
@@ -11,9 +10,10 @@ import {
DialogClose,
DialogFooter,
DialogHeader,
} from "@components/Dialog";
import { Alert } from "@components/ui/Alert";
import Button from "@components/ui/Button";
} from "@calcom/ui/Dialog";
import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
interface Props {
teamId: number;

View File

@@ -5,11 +5,12 @@ import Link from "next/link";
import { TeamPageProps } from "pages/team/[slug]";
import React from "react";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import { WEBSITE_URL } from "@calcom/lib/constants";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import Avatar from "@components/ui/Avatar";
import Button from "@components/ui/Button";
import Text from "@components/ui/Text";
type TeamType = TeamPageProps["team"];
@@ -27,7 +28,7 @@ const Team = ({ team }: TeamPageProps) => {
"space-y-4",
"p-4",
"min-w-full sm:min-w-64 sm:max-w-64",
"bg-white dark:bg-neutral-900 dark:border-0 dark:bg-opacity-8",
"bg-white dark:bg-neutral-800 dark:border-neutral-700 dark:bg-opacity-8",
"border border-neutral-200",
"hover:cursor-pointer",
"hover:border-brand dark:border-neutral-700 dark:hover:border-neutral-600",
@@ -51,7 +52,7 @@ const Team = ({ team }: TeamPageProps) => {
<div>
<Avatar
alt={member.name || ""}
imageSrc={getPlaceholderAvatar(member.avatar, member.username)}
imageSrc={WEBSITE_URL + "/" + member.username + "/avatar.png"}
className="-mt-4 h-12 w-12"
/>
<section className="mt-2 w-full space-y-1">

View File

@@ -0,0 +1,14 @@
import { useSession } from "next-auth/react";
import { FC, Fragment } from "react";
type AdminRequiredProps = {
as?: keyof JSX.IntrinsicElements;
};
export const AdminRequired: FC<AdminRequiredProps> = ({ children, as, ...rest }) => {
const session = useSession();
if (session.data?.user.role !== "ADMIN") return null;
const Component = as ?? Fragment;
return <Component {...rest}>{children}</Component>;
};

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