Compare commits

...
63 Commits
Author SHA1 Message Date
ba04533de3 Linting fixes round #1 (#2906)
* Fixes round #1

* disabled any warning for intentional typing of AsyncReturnType

* Whacked MetaMask add / remove button

* types, not great, not terrible, better than any

* Fixed typo in CheckboxField and wrapped description in <label>

* Feedback

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-06 18:24:37 +00:00
zomars 547b40913f Merge branch 'production'
# Conflicts:
#	.env.example
#	.github/workflows/e2e-embed.yml
#	apps/web/components/EmptyScreen.tsx
#	apps/web/components/SettingsShell.tsx
#	apps/web/components/Shell.tsx
#	apps/web/components/booking/BookingListItem.tsx
#	apps/web/components/booking/pages/AvailabilityPage.tsx
#	apps/web/ee/components/LicenseRequired.tsx
#	apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx
#	apps/web/ee/components/support/HelpMenuItem.tsx
#	apps/web/lib/emails/templates/attendee-request-reschedule-email.ts
#	apps/web/lib/emails/templates/attendee-rescheduled-email.ts
#	apps/web/lib/emails/templates/attendee-scheduled-email.ts
#	apps/web/lib/emails/templates/organizer-request-reschedule-email.ts
#	apps/web/lib/emails/templates/organizer-rescheduled-email.ts
#	apps/web/lib/emails/templates/organizer-scheduled-email.ts
#	apps/web/pages/_app.tsx
#	apps/web/pages/apps/installed.tsx
#	apps/web/pages/event-types/[type].tsx
#	apps/web/public/static/locales/en/common.json
#	apps/web/server/routers/viewer/bookings.tsx
#	packages/app-store/vital/lib/templates/attendee-request-reschedule-email.ts
#	packages/app-store/vital/lib/templates/base-template.ts
#	packages/app-store/vital/lib/templates/organizer-request-reschedule-email.ts
#	packages/app-store/wipemycalother/lib/templates/attendee-request-reschedule-email.ts
#	packages/app-store/wipemycalother/lib/templates/base-template.ts
#	packages/app-store/wipemycalother/lib/templates/organizer-request-reschedule-email.ts
#	packages/ee/server/checkLicense.ts
#	packages/emails/email-manager.ts
#	packages/embeds/embed-core/package.json
#	packages/embeds/embed-snippet/package.json
#	packages/embeds/vite.config.js
#	packages/lib/constants.ts
#	packages/types/environment.d.ts
#	yarn.lock
2022-06-06 11:58:32 -06:00
Omar Lópezandkodiakhq[bot] 15d5333cf6 Refactor emails to use JSX as templating engine (#2915)
* Init Maizzle

* Initial template JSX conversion and testing

* WIP

* WIP

* WIP

* WIP

* WIP

* Migrated AttendeeRescheduledEmail

* WIP

* WIP

* DRY

* Cleanup

* Cleanup

* Cleanup

* Migrate feedback email

* Migrates ForgotPasswordEmail

* Migrates OrganizerCancelledEmail

* Migrated OrganizerLocationChangeEmail

* Formatting

* Migrated AttendeeRequestRescheduledEmail

* Migrates OrganizerPaymentRefundFailedEmail

* Migrates OrganizerRequestEmail

* Migrates OrganizerRequestReminderEmail

* Fixes type-check

* Moved email-manager to package

* Import fixes

* Removed duplicate email code from vital app

* Removed duplicate email code from wipemycal

* Build/type fixes

* Fixes web email imports

* Fixes build

* Embed build fixes

* Update AttendeeAwaitingPaymentEmail.tsx

* Update default-cookies.ts

* Revert "Embed build fixes"

This reverts commit 8d693e99aca6dfe92d5cbb27ffa962545c3e9389.

* Embed build fixes

# Conflicts:
#	packages/embeds/embed-core/package.json

* dep and email date fixes

* Update attendee-scheduled-email.ts

* Update package.json

* Update [...nextauth].tsx

* Update email.ts

* Prevents /api/email on production builds

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-06 17:49:56 +00:00
Joe Au-Yeung ad101e0a31 Address feedback of feedback feature (#2981) 2022-06-06 13:40:02 -04:00
Omar Lópezandkodiakhq[bot] 12d66cb9df Booking confirm endpoint refactoring (#2949)
* Adds new default handler and responder

* Moved confirm endpoint

* Fixes availability for unconfirmed bookings

* Cleanup

* Update _patch.ts

* Prevent too much diffs

* Adds missing BookingStatus

* Migrates confirmed & rejected to status

* Adds requiresConfirmation icon to listing

* Adds booking status migration

* Adds migrations to remove confirmed/rejected

* Undo refactor

* Sets the organizer as "accepted" in gCal

* Update getBusyTimes.ts

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-06 10:54:47 -06:00
3b321e5d3c New Crowdin translations by Github Action (#2980)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-06-06 17:33:13 +02:00
Carina Wollendorfer d0ac4d41ed fix language issue on booking page (#2973) 2022-06-06 14:30:41 +01:00
983c970069 Add more variables to event names and a dynamic field (#2898)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-06-06 08:48:13 -04:00
3189f8b77a New Crowdin translations by Github Action (#2966)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-06-06 12:07:20 +02:00
Hariom Balharaandkodiakhq[bot] 079016052d Add embed preview tests (#2969)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-06 12:06:27 +02:00
Hariom Balhara f0a5b7734f [Hotfix]Fixes 3 issues related to 404 links (#2977) 2022-06-06 09:41:11 +00:00
Hariom Balhara 03e787ad5a Fixes 3 issues related to 404 links (#2979) 2022-06-06 11:40:56 +02:00
Syed Ali Shahbaz dd09854932 Hotfix: fixed callback URL for team invite email (#2978) 2022-06-06 14:59:46 +05:30
Syed Ali Shahbaz b9428301b6 Hotfix: fixed callback URL for team invite email (#2975) 2022-06-06 14:07:53 +05:30
zomars 2344a4da13 Update yarn.lock 2022-06-05 16:42:26 -06:00
zomars 4652878e6c tsc fixes 2022-06-05 16:35:39 -06:00
zomars 4efc2914af Embed build fixes 2022-06-05 13:55:58 -06:00
zomars 9866e12a84 Update default-cookies.ts 2022-06-05 13:54:13 -06:00
github-actions[bot]andCrowdin Bot 8998e41104 New Crowdin translations by Github Action (#2964)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-06-05 09:00:08 +02:00
zomars 0a64715d84 Fallbacks to Cal Video when missing credentials for location 2022-06-04 11:32:43 -06:00
zomars 879619e075 Fallbacks to Cal Video when missing credentials for location 2022-06-04 11:23:56 -06:00
Hariom Balhara 16cab18163 Fix preview not working (#2962) 2022-06-04 07:31:13 -06:00
456ba9db04 fix: saml update/delete only owner team (#2948)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-04 07:31:10 -06:00
Hariom Balhara 1c89afc7ad Fix preview not working (#2962) 2022-06-04 07:17:02 -06:00
16836d0cfb fix: saml update/delete only owner team (#2948)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-03 23:13:50 +00:00
zomars 2c65e39482 Ignores vital for now 2022-06-03 14:18:43 -06:00
65dc7c5b65 New Crowdin translations by Github Action (#2959)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-06-03 22:04:54 +02:00
eb02efb25a New Crowdin translations by Github Action (#2834)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-06-03 22:01:49 +02:00
864a873c57 Images in slider will have fixed height, variable width (#2916)
* Images in slider will have fixed height, variable width

* fix spacing

* remove extra whitespace

* fix whitespace lint errors

* fix whitespace lint error

* fix width for images in app store slider

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-06-03 22:01:00 +02:00
Peer Richelsen fcba3bec03 added email to ee helper (#2958) 2022-06-03 21:59:40 +02:00
zomars 4569a96e78 Update index.ts 2022-06-03 13:21:46 -06:00
zomars debbd980d9 Fixes preview deploys 2022-06-03 12:48:47 -06:00
zomars d5e40faeb5 Adds booking reportUsage for enterprise 2022-06-03 12:33:53 -06:00
zomars 10edc1f4ac Update next.config.js 2022-06-03 10:50:48 -06:00
Peer Richelsen 42c49a8263 renamed integratin to app and extracted into common.json (#2951) 2022-06-03 14:57:50 +00:00
7b0d5811d6 Improve meeting url information in booking detail view (#2943)
* change meeting url text for booking detail view

* improve for wen conferencing details for different booking status

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-03 11:28:33 +00:00
9d1e6f23db Fixing spaces and wraps for event type legend (#2941)
* Fixing spaces and wraps

* Spacing wrapped items

* fix lint errors

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-06-03 11:15:29 +00:00
Carina WollendorferandCarinaWolli b4f3680876 disable confirm button when recurring mutation is loading (#2950)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-06-03 12:52:10 +02:00
zomars 7536473c9d Allows telemetry opt-out 2022-06-02 14:06:13 -06:00
zomars fd8e4ef890 licenseCheck fixes 2022-06-02 13:49:12 -06:00
zomars c8a16ce43b Call prod console only on prod non staging deploys 2022-06-02 13:40:16 -06:00
zomars 887bd66a33 All prod builds should call prod console 2022-06-02 13:34:32 -06:00
zomars eeb9ff662e Adds cors to checkLicense 2022-06-02 13:20:48 -06:00
zomars 262df1a1b2 Updates console 2022-06-02 13:20:35 -06:00
zomars 3919d87486 Adds license to telemetry 2022-06-02 11:53:02 -06:00
zomars 6f203d14e3 Submodule updates 2022-06-02 10:20:29 -06:00
Vladimir KlimontovichandArt Sk 456a4ba581 Telemetry improvements (#2935)
* next-collect initial setup

* naming changes

* WIP

* WIP: added cookie name

* telemetry update

* fixes

* telemetry eventTypes fix

* tmp jitsu config for testing

* tmp jitsu key update (for tests)

* deploy commit

* cookieName fixes

* telemetry credentials update

* NextCollect updated to latest canary; added TELEMETRY_KEY to config

* TELEMETRY_KEY fix

removed browser_user_agent field from event

* removed _middleware.ts for test deploy

* _middleware.ts restored

* next-collect version bump

* yarn.lock fix

* Added license consent property, set default telemetry endpoint

* Switched to stable version of next-collect; restored LicenseProvider accidentally deleted during merge

* Switched to stable version of next-collect; restored LicenseProvider accidentally deleted during merge

* Updated to latest version of next-collect

* - Updated to latest version of next-collect
- Few improvements in event collection: isTeamBooking for all events, page_url for all events
- Do not send second page event on re-render of /team/[slug]

* Revert booking confirmed tracking

* Applied prettier + fix lint

Co-authored-by: Art Sk <kirsan007@gmail.com>
2022-06-02 10:19:01 -06:00
60d6c7be51 fix: owners can't be edited by admins (#2942)
* fix: owners can't be edited by admins

* fix: removec omment

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-02 09:39:23 -06:00
zomars 0f273c5efc Update package.json 2022-06-01 15:46:48 -06:00
33a45a8af5 Installed Apps page revamp (#2751)
* First round of changes

* Some missing styling

* Last round of core changes

* Color tweaks

* Improving code readability

* Reverting unneeded changes

* Reverting yarn.lock

* Removing yarn.lock

* Empty state updated

* Fixing webhook test

* Cleaning up code

* Fixing test and simplifying code a bit

* Merging API Keys into developer section

* Unifying Empty Screen with monorepo version

* Cleaning up

* Installed apps logic consistency + cleaning up

* Type fixes

* Apply suggestions from code review

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

* Improvements, still WIP

* Update apps/web/pages/apps/installed.tsx

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

* Apply suggestions from code review

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

* App active install status

* Apple Calendar setup, Daily.co preinstalled

* Final pass

* Minor tweaks

* Conflicts with migration ignored

* Fixing merge

* Fixing merge yet again

* Adopting main changes

* Removing unneeded data-testid

* Fixing reported bugs

* Simplifying webhook query

* Moving teams settings tab to second

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-06-01 14:24:41 -03:00
sean-brydon 6179b0cc56 Fixing Spacing + text color (#2938)
* Fixing Spacing + text color

* Fix overflow

* Fixing mobile text color
2022-06-01 14:58:46 +02:00
alannncandkodiakhq[bot] 9ded50b5fc Fix/infinite booking list (#2931)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-06-01 09:33:14 +05:30
7c34be272f Invalid Payment URL gives 500 (#2857)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>
2022-05-31 14:40:12 +01:00
6145c7ad90 Bug - Admin Shell Duplicated (#2933)
* Fix additional shell in admin

* Update headings

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-05-31 14:36:20 +01:00
Syed Ali Shahbaz 5225fc4317 A few booking page UI fixes (#2932) 2022-05-31 13:20:51 +02:00
Hariom Balhara 514cff7b3c Fix: Embed Tests - Handle superfast switching to next month bug (#2929)
* Wait for more time for next month dates to fully render

* Add comments

* Wait for rendering of current month as well

* Fix retries

* Fix speed issue with non embed tests asw ell
2022-05-31 09:32:41 +00:00
Hariom Balhara eb6e092a21 Improve dx command (#2918) 2022-05-30 17:47:58 -06:00
Hariom Balhara 7d2c0ab405 Improve dx command (#2918) 2022-05-30 23:11:22 +01:00
+22 01631e808f v1.6.2 (#2926)
* Turbo fixes

* Make apps single pages public

* fixed /booking skeleton (#2722)

* fixed /booking skeleton

* nit

* Type fixes

* Test fixes

* Update playwright.config.ts

* More test fixes

* Update dynamic-booking-pages.test.ts

* add invite link to Zapier setup page (#2696)

* add invite link and toaster to zapier setup page

* create env variable for invite link and save in database

* fetch invite link form getStaticProps

* add getStaticPath method

* clean code

* Moves app setup and index page

* Moves Loader to ui

* Trying new way to handle dynamic app store pages

* Cleanup

* Update tailwind.config.js

* zapier invite link fixes

* Tests fixes

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>

* Add more embed events (#2719)

* Add more embed events

* Add more embed events

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

* adds availability select loader (#2718)

* Improve logs and Fix unwanted 500 to reduce noise in logs (#2674)

* Improve logs

* Fix unintentional 500

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

* Change date format for RecurringBookings (#2707)

* Change date format for RecurringBookings

* Missing bookingId query param

Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* added giphy description (#2730)

* fixes #2732 (#2732)

* Hotfix : Fix Infinite loading of Bookings (#2729)

* Add more embed events

* Add more embed events

* Fix nextCursor calculation logic

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

* Hotfix: Success page for recurring event (#2725)

* Merge pull request #2672 from calcom/main

v1.5.4

* Turbo fixes

* Make apps single pages public

* Fix preview.html not built and thus served during depooy (#2713)

* Hotfix: Success page layout broken due to duplicate "When" (#2716)

* Update BookingPage.tsx

* Reverting unchaged lines

* Fixing recurrenceRule for ICS files

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

* Fix preview.html not built and thus served during depooy (#2727)

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

* Allow deletion of a disabled event (#2737)

* allows deletion of disabled event

* some visual fixes

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

* Multiple E2E improvements

* Parallelizes some tests

* Update booking-pages.test.ts

* E2E and paid bookings fixes

* Add 'free' and 'workingElsewhere' as a non-blocking event (#2652)

* Add 'free' and 'workingElsewhere' to non-blocking event - this will allow bookings at these times

* Update CalendarService.ts

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

* TODO marks blocking test to be fixed

* Update testUtils.ts

* Update testUtils.ts

* getBusyTimes consolidation

* Fixes delete-me test

* E2E fixing attemps

* Adjusting Zapier endpoints for publishing integration (#2728)

* add /me endpoint for zapier API testing

* remove cacellationReason from listBookings response

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
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>

* Fix reschedule not happening in calendar if two calendards are there (#2733)

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

* Increases timeout temporarily

* Merge pull request #2745 from calcom/apps/multiple-categories

Allow apps to belong to multiple categories

* Build fixes

* Populate msteams key in db (#2743)

* Populate msteams key in db

* Fix calendar credentials to teams

* Clarify account dialog

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>

* Improving Email DRYness (#2486)

* Email DRY

* WIP

* Improve email DRYness

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

* E2E fixtures (#2747)

* Sign in button should be changed or disabled after click #2654 (#2749)

Co-authored-by: gitstart <gitstart@users.noreply.github.com>

* Fixing ESLint warnings (#2723)

* Fixing warnings

* Reverting and disabling ESLint in some cases

* Reverting Next Images

* Reverting file, bad merge

* Targeting ESLint to line

* Additional warnings

* New warning squished

* More tweaks and major fixes

* Uneeded conf

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

* Fix/avoid multiple schedule deletions (#2602)

* Prevents users from deleting the same schedule multiple times due to delay before the schedule disappears. It also applies the same fix to team disband.

Schedule deletion:
![schedule_deletion_new_behaving](https://user-images.githubusercontent.com/42497300/165126805-b3090268-c1a6-418a-b06e-06bd8446da03.gif)

Team disband:
![team_disband_new_behaving](https://user-images.githubusercontent.com/42497300/165127043-7e083e94-e4c9-4e88-90a2-47d31bdd92e6.gif)

Fixes issue [#2569](https://github.com/calcom/cal.com/issues/2569)

Bug fix (non-breaking change which fixes an issue)

**apps/web/components/LightLoader.tsx** → this file was created in order to make a light color loading spinner available. It's necessary when we need to display a loading spinner above dark backgrounds.

**apps/web/components/availability/ScheduleListItem.tsx** → this component was created in order to give a schedule list item its own state.

* Removing a "setTimeout" that was only used for testing purposes

* Adding a code review suggestion to my modifications

* Changing loading style

* Cleanup

* Avoids using unnecessary state

* Revert "Adding a code review suggestion to my modifications"

This reverts commit b5e40062d71157ca1d9384fb1f5c30d50625809d.

* Reverts some changes

* Renames isLoading

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

* Added check on create eventtype to see is user has slug that already exists (#2757)

* Added check on create eventtype to see is user has slug that already exists, added error check on onError

* revert yarn.lock back

* Catches prisma known error instead of making an additional query

Co-authored-by: zomars <zomars@me.com>

* Fix white border (#2761)

* fix: remove hardcoded redirect in signin url email verification (#2764)

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>

* Bad UX when user wants to set the default Event Type Title #2245 (#2760)

Co-authored-by: gitstart <gitstart@users.noreply.github.com>

* Skip sending emails in E2E

* Users Phone Number Option (#2669)

* Users Phone Number Option

* Implemented improvments

* Add validation to form

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

* Fix/unpaid unconfirmed (#2553)

* Fix merge errors

* Errors prettier/prettier

* Update apps/web/pages/api/book/event.ts

Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com>

* Update apps/web/pages/api/book/event.ts

Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com>

* Update apps/web/pages/api/integrations.ts

Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com>

* Fix merge errors

* Errors prettier/prettier

* Update apps/web/pages/api/book/confirm.ts

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

* Modal window before delete stripe integration

* ESLint Report

* Test fixes

Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>

* Mutually exclusive options (#2755)

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Readd steps to create a new user #2665 (#2759)

Co-authored-by: gitstart <gitstart@users.noreply.github.com>

* remove redundant conditional expressions (#2756)

* remove redundant conditional expressions

* remove redundant conditional expression

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

* Fix adds redirect callback that support app.cal.com (#2768)

* Fix adds redirect callback that support app.cal.com

* Update apps/web/pages/api/auth/[...nextauth].tsx

Check origin of website and baseurl

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

* fix: lint issue extra space removed

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* respect local set timezone and update url on mismatch (#2506)

* ensure `timeZone()` will make its way to the URL

fixes https://github.com/calcom/cal.com/issues/2482

* keep `timeZone()` and the offset from URL in sync

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>

* Squashed commit of the following:

commit 27540b09ce
Author: Agusti Fernandez Pardo <me@agusti.me>
Date:   Mon May 16 17:34:13 2022 +0200

    fix: remove hardcoded redirect in signin url email verification (#2764)

    Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>

commit ae15a7d739
Author: Hariom Balhara <hariombalhara@gmail.com>
Date:   Tue May 10 14:30:43 2022 +0530

    Fix time issue

commit 2a5a89fe50
Author: Leo Giovanetti <hello@leog.me>
Date:   Wed May 11 10:21:46 2022 -0300

    Missing fix for success page

commit 2ce1e78053
Author: Leo Giovanetti <hello@leog.me>
Date:   Wed May 11 10:12:59 2022 -0300

    Hotfix: Success page for recurring event (#2725)

    * Merge pull request #2672 from calcom/main

    v1.5.4

    * Turbo fixes

    * Make apps single pages public

    * Fix preview.html not built and thus served during depooy (#2713)

    * Hotfix: Success page layout broken due to duplicate "When" (#2716)

    * Update BookingPage.tsx

    * Reverting unchaged lines

    * Fixing recurrenceRule for ICS files

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

commit 2d6d1cb444
Author: Hariom Balhara <hariombalhara@gmail.com>
Date:   Tue May 10 14:49:46 2022 +0530

    Hotfix: Success page layout broken due to duplicate "When" (#2716)

commit ef68f4f4f8
Author: Hariom Balhara <hariombalhara@gmail.com>
Date:   Tue May 10 10:54:20 2022 +0530

    Fix preview.html not built and thus served during depooy (#2713)

commit 18c28cc3fd
Author: zomars <zomars@me.com>
Date:   Mon May 9 16:17:07 2022 -0600

    Make apps single pages public

commit d40e8caff9
Author: zomars <zomars@me.com>
Date:   Mon May 9 16:08:03 2022 -0600

    Turbo fixes

commit 3161cc4d45
Merge: ed808c3be 4099a477d
Author: zomars <zomars@me.com>
Date:   Mon May 9 14:58:33 2022 -0600

    Merge branch 'main' into production

commit ed808c3be6
Author: Omar López <zomars@me.com>
Date:   Mon May 9 14:56:23 2022 -0600

    Merge pull request #2672 from calcom/main

    v1.5.4

* Typo

* Typo

* Update apps/web/pages/apps/categories/[category].tsx

* Apply suggestions from code review

* Alert to describe exclusion of options (#2770)

* Alert to describe exclusion of options

* Update apps/web/pages/event-types/[type].tsx

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

* Update apps/web/components/eventtype/RecurringEventController.tsx

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

* Formatting

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>

* fix: split time correctly if the local working hours are just across mid night (#2766)

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

* Update crowdin.yml

* New Crowdin translations by Github Action (#2773)

* New Crowdin translations by Github Action

* Update vital.json

* Update vital.json

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Omar López <zomars@me.com>

* Add Google cal extneral calendar id to booking reference (#2671)

* Set google cal event id to use our uid

* Save calendar external id to bookingRef

* Pass external calendar ids to update and delete

* Create migration

* Fix type errors

* Fix prisma url

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>

* v1.6

* v1.6

* 2FA submit disabled (#2790)

* fixing the hyperlink for open startup (#2777)

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

* Add login event (#2784)

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>

* Fixes infinite loop

* Fixes infinite loop

* Fixes infinite loop

* Update all Yarn dependencies (2022-05-16) (#2769)

* Update all Yarn dependencies (2022-05-16)

* Upgrade dependencies

* Removes deprecated packages

* Upgrades deps

* Updates submodules

* Update yarn.lock

* Linting

* Linting

* Update website

* Build fixes

* TODO: fix this

* Module resolving

* Type fixes

* Intercom fixes on SSG

* Fixes infinite loop

* Upgrades to React 18

* Type fixes

* Locks node version to 14

* Upgrades daily-js

* Readds missing types

* Upgrades playwright

* Noop when intercom is not installed

* Update website

* Removed yarn.lock in favor of monorepo

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>

* Create ci.yml

* Update ci.yml

* Reintroduces typescript-eslint

Buckle up!

* Type fixes

* Update ci.yml

* Update api

* Update admin

* Reusable inferSSRProps

* Linting

* Linting

* Prisma fixes

* Update ci.yml

* Cache testing

* Update e2e.yml

* Update DatePicker.tsx

* Update e2e.yml

* Revert "Linting"

This reverts commit adf817766e.

* Revert "Linting"

This reverts commit 1b59dacd64.

* Linting

* Update e2e.yml

* Ci updates

* Add team Id to hash url (#2803)

* Fix missing tabs - Embed (#2804)

* Fix missing tabs

* Fix Eslint error

* Fix Eslint errors

* Add import statement (#2812)

* Add import statement

* Update apps/docs/next.config.js

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

* Show success page if booking was deleted on calendar (#2808)

* Add exception to 410

* Fix type error

* Add GoogelCalError type

* only show invite link for app.cal.dev (#2807)

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

* fix: update eslint config to test .ts and .js separately (#2805)

* fix: update eslint config

* fix: update ts ignore

* fix: update eslint config

* Update TeamAvailabilityScreen.tsx

* Type fixes

* Update useIntercom.ts

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

* fix: sync api to latest commit (#2810)

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Embed React improvements (#2782)

* Add off support. Add getApi export.

* Add publish command

* Add embed-snippet in prod deps

* Update README

* Update package.json

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>

* Consolidates test-results

* Add vscode tasks.json (#2801)

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

* save additional inputs as json + view details of booking (#2796)

* move custom inputs from description to own json object

* show custom inputs on success page

* fix type error

* add custom inputs to email and webhook

* add custom inputs to all emails

* add values for custom inputs when rescheduling

* add custom input everywhere description is shown

* fix bug with boolean value

* fix issues with null values

* disable custom inputs and add notes for organizer

* don't show custom input with empty string

* don't show custom inputs with empty string in calender event and email

* add link to booking details page

* redirect to success page to see booking details

* add functionality to cancel and reschedule booking

* fix bookings that require confirmation

* clean code

* fix infinite lopp in useEffect of success page

* show web conference details message when integration as location

* improve design of cancelling event

* clean code

* disable darkmode for organizer on booking details page

* fix dark mode for cancelling booking

* fix build error

* Fixes infinite loop

* Fixes infinite loop

* Fixes infinite loop

* Update all Yarn dependencies (2022-05-16) (#2769)

* Update all Yarn dependencies (2022-05-16)

* Upgrade dependencies

* Removes deprecated packages

* Upgrades deps

* Updates submodules

* Update yarn.lock

* Linting

* Linting

* Update website

* Build fixes

* TODO: fix this

* Module resolving

* Type fixes

* Intercom fixes on SSG

* Fixes infinite loop

* Upgrades to React 18

* Type fixes

* Locks node version to 14

* Upgrades daily-js

* Readds missing types

* Upgrades playwright

* Noop when intercom is not installed

* Update website

* Removed yarn.lock in favor of monorepo

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>

* Create ci.yml

* Update ci.yml

* Reintroduces typescript-eslint

Buckle up!

* Type fixes

* Update ci.yml

* Update api

* Update admin

* Reusable inferSSRProps

* Linting

* Linting

* Prisma fixes

* Update ci.yml

* Cache testing

* Update e2e.yml

* Update DatePicker.tsx

* Update e2e.yml

* Revert "Linting"

This reverts commit adf817766e.

* Revert "Linting"

This reverts commit 1b59dacd64.

* Linting

* Update e2e.yml

* Ci updates

* Add team Id to hash url (#2803)

* Fix missing tabs - Embed (#2804)

* Fix missing tabs

* Fix Eslint error

* Fix Eslint errors

* Add import statement (#2812)

* Add import statement

* Update apps/docs/next.config.js

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

* Show success page if booking was deleted on calendar (#2808)

* Add exception to 410

* Fix type error

* Add GoogelCalError type

* only show invite link for app.cal.dev (#2807)

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

* fix: update eslint config to test .ts and .js separately (#2805)

* fix: update eslint config

* fix: update ts ignore

* fix: update eslint config

* Update TeamAvailabilityScreen.tsx

* Type fixes

* Update useIntercom.ts

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

* fix: sync api to latest commit (#2810)

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Embed React improvements (#2782)

* Add off support. Add getApi export.

* Add publish command

* Add embed-snippet in prod deps

* Update README

* Update package.json

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>

* Consolidates test-results

* Type fixes

* Abstracts minimal booking select

* Type fixes

* Update listBookings.ts

* Update common.json

* Update bookingReminder.ts

* Consolidates isOutOfBounds

* Update webhookResponse-chromium.txt

* Update TableActions.tsx

* Type fixes

* Update BookingPage.tsx

* Update webhookResponse-chromium.txt

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: iamkun <kunhello@outlook.com>
Co-authored-by: Agusti Fernandez Pardo <me@agusti.me>
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Update check-types.yml

* adding organizer as attendee to google calendar events (#2779)

* Fix auto-select and close of dropdown (#2819)

* fixes dynamic color and typefix for tfunction after react upgrade (#2821)

* New Crowdin translations by Github Action (#2791)

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

* Relocates admin to console

# Conflicts:
#	apps/admin

* Relocates admin to console

* Fix login submit (#2849)

* fix: long string overflowing calendar div (#2842)

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>

* Adding labels (#2783)

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

* Wrong username is identified if query params are present and user doesn't exist (#2838)

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>

* Meet/Zoom Email Clarification (#2828)

* Add clarificaiton to email

* Update apps/web/lib/emails/templates/organizer-scheduled-email.ts

* Add to attendee scheduled email

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* Request the inclusion Assuncion Time Zone (#2840)

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>

* Fix UI of dialog (#2788)

* removed large mandatory height and scroll

* added z index using css

* cleanup

* fixed TS errors

* extract dialog out of dropdown

* Adds custom loading text to confirmation dialog

* rename update

* utilizing mutation loading state

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

* Hotfix: Fixing Security Issues (#2848)

* Fixing Privilege Escalation

* Fixing critical obj ref in availability

* Fixing reschedule security issue

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

* Merge production to main

* Update vital.json

* Update vital.json

* Fix login page CTA disabled state (#2832)

* fix(ui/login): better disabled state for login CTA

The fix involves tracking the form submission with a dedicated state `submitInProgress` with React.
`formState` from `react-hook-form` does not take time taken for the network call into account. For example,
if the api takes 5 seconds to complete, we would expect the `formState.isSubmitting` to be true for `5`
seconds. But, surprisingly this is not the case and `formState` from `react-hook-form` resolves
immediately after it makes a successful connection to the endpoint.

A dedicated state (with `useState`) is introduced that is enabled when the user clicks on the login CTA, and disabled when the api call is resolved, either successfully or with an error.

* Update login.tsx

* Update login.tsx

* Fixes isSubmitting state

Co-authored-by: zomars <zomars@me.com>

* Playwright binaries shouldn't be on deps

* Playwright binaries shouldn't be on deps

* Fix infinite renders on event-type edit page (#2820)

* Updates submodules

* Makes sure to hash post-install cache

* Add seats to event types (#2485)

* Add seatsPerTimeSlot to event type schema

* Add seats per time slot to event type form

* Book event and render seats

* Pass booking uid for seats

* Disable requires confirmation if seats are enabled

* Fix type errors

* Update submodules

* Fix type errors

* Fix type errors

* Fix duplicate string

* Fix duplicate string

* Fix schema and migration file

* Fix render seats

* Fix bookinguid typos

* Remove console.log

* Fix type error

* Fix mobile formatting

* Update apps/web/lib/hooks/useSlots.ts

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

* Update apps/web/lib/hooks/useSlots.ts

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

* Added translation for seats available text

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

* Redesign help menu item (#2710)

* Seperate help menu item and contact menu item

* Add menu items

* Install react-popover

* Render contact only if support keys are present

* Adjust contact support links

* Add translations

* Add embed changes

* Adjust menu if helped is pressed

* Add items to help menu

* Change button color on selection

* Create endpoint

* Create feedback table

* Create migration file

* Write feedback to db

* Remove logs

* Add response message

* Send feedback email

* Disable submit if no rating and after submit

* Add translations

* Fix padding

* Clean up

* Clean up

* Add user feedback email to .env example

* Lint fixes and styles

* Changed onClick function to a named function and fix style

* Fix ids order

* Removed commented code and changed textarea id and name

* Fix id orders

* Change to AND operator

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

* Add user relation to feedback

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

* Add migration files

* Change rating to strings

* Change rating to strings

* Fix type errors

* WIP success & error messages

* Change success and error to boolans

* Style messages

* Add await

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

* Remove duplicate string

* Refactor import statement

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

* Change opacity of emojis

* added support@cal.com email for feedback

* Add success toast

* Update .env.example

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

* Add tCRP route

* tCRP send email

* tCRP send email

Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fixed labels for seats, removes shadow from event-type inputs (#2862)

* fixed labels for seats

* minor fixes, removed shadow from all event-type inputs

* Render input field on pro accounts (#2859)

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

* Submodule sync

* sec-001 fixes (#2866) (#2867)

* sec-001 fixes (#2866)

* sec-001 fixes

* Typo

* Fixes paid bookings

# Conflicts:
#	apps/web/ee/components/stripe/Payment.tsx

* Hotfix: Fixing Security Issues (#2848) (#2850)

* sec-001 fixes (#2866)

* sec-001 fixes

* Typo

* Fixes paid bookings

* fixes issues with date of recurring events on booking detail page (#2872)

* fixes date not showing for confirmed recurring event

* only show all events on upcoming bookings

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* Resolve if can't find event on google cal (#2860)

* Resolve if can't find event on google cal

* Change order of  console.error

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

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

* Disable Impersonation Option (#2880)

* Disable Impersonation

* Update Description Copy

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

* Fix deleted event when on different cal (404) (#2882)

* Fix/email shows false (#2879)

* Webhook sec fixes (#2883)

* Webhook sec fixes

* Revert changes

* Feat/Display location information publicly  (#2752)

* Updating checkbox field to reflect new designs

* Include Infobadge option checkbox

* Checkbox Field + i18n

* Default checked - true

* Sync with router

* Extracting Types

* Update filtering logic

* Add UI to booking page

* Default address/link

* Update hashedlink page

* Tidy up

* Video icon

* Add nullish check

* Update to use RHF controller

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

* Fix 500 errors if data is invalid (#2886)

* Check in middleware to ensure authorization for all endpoints (#2885)

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

* Fix bug to check opt in bookings (#2889)

* Fix bug to check opt in bookings

* Implemented value, onChange instead of form access

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

* remove unused imports (#2892)

* Cascades impersonations on user delete (#2891)

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

* Ensures json fields on each call (#2893)

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

* Redundant conditional expression (#2894)

* remove redundant conditional expressions

* remove redundant conditional expressions

* License server (#2379)

* WIP License server

* WIP

* Moves locations to App Store and Core

* LocationType fixes

* Runs db migrations post-deploy

* WIP

* WIP

* Cleanup

* WIP

* WIP

* Decouples translations from NavTabs

* Adds admin submodule

* Adds admin submodule

* Sync dependencies

* WIP

* WIP

* Updates submodules

* Renames package

* Updates submodules

* Adds scripts for console

* Updates license checker URL

* Updates admin

* Adds staging/prod admin console links

* Update yarn.lock

* Update NavTabs.tsx

* WIP

* Update admin

* WIP

* Adds hint to InputField

* Update admin

* Adds turbo admin dependecies

* Update admin

* Prevents redirection on form submit

* Form warning fixes

* Update admin

* Form fixes

* Update yarn.lock

* Update admin

* Update admin

* Update admin

* Adds withLicenseRequired HOC

* Adds LicenseRequired to EE components

* Admin deploy fix?

* Updates submodules

* Use relative inside lib

* type fixes

* Fixes turbo race condition

* Relocates admin to console

* Relocates admin to console

* Update console

* Update api

* Update turbo.json

* Update ErrorBoundary.tsx

* Update defaultEvents.ts

* Update checkLicense.ts

* Update yarn.lock

* Skip on E2E

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

* Update turbo.json

* Reverts license check on payment

Since it's a public page

* Submodule sync

* fix: overflowing text on teams page - using tailwind line-clamp (#2843)

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>

* Fix @calcom/embed-react installation with TS project (#2870)

* Fix failing builds (#2908)

* Update turbo.json

* Change location of booking (#2658)

* add functionality to change location in booking and send out mail

* add i18n

* change location with dropdown like in event-types

* small fixes and code clean up

* clean code

* improve format of current Location string

* clean code

* clear selection when dialog closed

* added mutation and changed props (first working verison)

* clean code

* clean code

* clean code

* clean code

* fix typo

* change maxHeight of select

* use useWatch for selectedLocation

* pass default values with props

* set current location directly in useState

* clear selected values when updating location

* fix trpc query for credentialst

* change icons for editing booking

* improve naming of variables

* remove unnecessary orderBy

* use locationOptionsToString method

* fix current location naming for Cal Video

* add phone input

* save phone number as location of booking

* remove input field for phone number for event-types

* fix redirection issue

* show previous selected location in event-type

* remove attendee number from selection for booking

* make first letter of location lowercase

* remove input field for attendee phone number

* clear Errors when changing location type

* set location details to optional

* clean code

* fixing issue that dropdown doesn't close when dialog opens

* clean code

* make overflow visibile in dialog

* fix existing bug with address not showing in event-type settings

* fix issue with losing focus after validation

* close rejection dialog

* small spelling fixes

* fix issue with LocationChangeEmail

* fix failing E2E test

* fix failing E2E test

* fix E2E test

* bug fix for saving user phone, and other minor changes

* merge main

* improve text

* fix UI of booking list

* Delete admin

* remove selection after update and submit

* add translation for error message

* add default values for checkbox

* add "your phone number" to locations on booking page

* remove duplicate attributes from viewer.bookings

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

* check if user is authorized to make changes to booking

* remove location string

* clan code for displayLocaitonPublicly checkbox

* fetch locationOptions on server side

* remove trpc query for credentials

* fix phone number input

* fix labels of host and attendee phone number for booking page

* Migrates edit location to tRPC

* Link elemnt should only be used in `a` tags

* Adds missin router

* Migrates locationOptions to tRPC query

* Type fixes

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>

* added checkly and twitch badges

* Update README.md

* areas of expertise test

* Update CONTRIBUTING.md

* Update README.md (#2912)

* Update CONTRIBUTING.md

* Update CONTRIBUTING.md

* Update CONTRIBUTING.md

* Update CONTRIBUTING.md

* Update CONTRIBUTING.md

* Update CONTRIBUTING.md

* remove unsed imports (#2895)

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

* Fix event-type preview links on Vercel Previews (#2919)

* Fix website url

* Fix avatar paths everywhere

* Fix linting errors

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

* Cleanup & removed usages of sparingly used Text/ files (#2904)

* Update CONTRIBUTING.md

* Update CONTRIBUTING.md

* Fixed typo in CheckboxField and wrapped description in <label> (#2924)

* Fixed typo in CheckboxField and wrapped description in <label>

* Make functionality identical to before

* Fixed use of infomationIconText

* Fix lint error (needs refactor, out of scope)

* Payment amount input leading 0 (#2836)

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>

* Allow less than 6 bookings and add proper min validation of 2+ (#2921)

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

* fix(app-store): Giphy Ux fixes (#2746)

* fix(app-store): Giphy Ux fixes

* Add search placeholder

* Min height image on event type page

* Use regex in zod validation and fix design issues for figma row 1

* Add paddings as per discussion with Ciarán

* Fix getGiphyApiKey nomenclature

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

* Update regex to be more readable and allow enter to search

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Add embed tests in CI (#2887)

* Add embed tests in CI

* Update e2e-embed.yml

* Run quick tests only

* Ignore certain folders

* Add embed-react tests as well

* Fix commands

* Dont run unit tests

* Update playwright.config.ts

* Update playwright.config.ts

* Update playwright.config.ts

* Create correct artifcats

* Create correct artifcats

* Fi ignore path

* Update package.json

* Add back embed-react typecheck

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

* remove subtitle (#2925)

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

* Fix booking detail page for cancelled events (#2905)

* fix booking detail page for cancelled events

* Fix typo

* fix bug when now attendees exist in booking

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

* v1.6.2

* Add reschedule reason to DB and emails (#2909)

* Write reschedule reason to db

* Add reschedule reason to emails

* Add reschedule reason to request reschedule email

* Add reschedule reason to request reschedule email

* Add reschedule reason to request reschedule email

* Add reschedule reason to request reschedule email

* Write reschedule reason to db

* Add reschedule reason to emails

* No longer using rescheduleReason in favor of cancellationReason

* Update apps emails accordingly with reschedule reason

* Update reschedule.test.ts

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>

* Update website

* Update HelpMenuItem.tsx

* Update event.ts

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: GitStart <1501599+gitstart@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Arthur Cruz <42497300+arthur1041@users.noreply.github.com>
Co-authored-by: Mitchell Moore <47459168+Mitchell-Moore@users.noreply.github.com>
Co-authored-by: Agusti Fernandez Pardo <me@agusti.me>
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: andreaestefania12 <19562383+andreaestefania12@users.noreply.github.com>
Co-authored-by: Miguel Nieto A <39246879+miguelnietoa@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Hashen <37979557+Hashen110@users.noreply.github.com>
Co-authored-by: buschco <colin@busch.dev>
Co-authored-by: iamkun <kunhello@outlook.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Ankit Gordhandas <agordhandas@gmail.com>
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: Ken Miller <kemiller@users.noreply.github.com>
Co-authored-by: Júlio Piubello da Silva Cabral <julio.piubello@gitstart.dev>
Co-authored-by: Arun Kumar <palerdot@users.noreply.github.com>
Co-authored-by: Shrey Gupta <connectwithshrey@gmail.com>
2022-05-30 15:36:15 -06:00
Omar López 3af6b3b6b3 Update event.ts 2022-05-30 14:43:15 -06:00
Omar López bba9b755bc Update HelpMenuItem.tsx 2022-05-30 13:43:21 -06:00
zomars ef523bda51 Update website 2022-05-30 13:41:57 -06:00
0761e8664a Add reschedule reason to DB and emails (#2909)
* Write reschedule reason to db

* Add reschedule reason to emails

* Add reschedule reason to request reschedule email

* Add reschedule reason to request reschedule email

* Add reschedule reason to request reschedule email

* Add reschedule reason to request reschedule email

* Write reschedule reason to db

* Add reschedule reason to emails

* No longer using rescheduleReason in favor of cancellationReason

* Update apps emails accordingly with reschedule reason

* Update reschedule.test.ts

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-05-30 13:40:29 -06:00
308 changed files with 7078 additions and 7929 deletions
+4 -4
View File
@@ -15,7 +15,8 @@
# - 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=''
# To enable enterprise-only features, fill your license key in here
# To enable enterprise-only features, fill your license key in here.
# @see https://console.cal.com
CALCOM_LICENSE_KEY=
# ***********************************************************************************************************
@@ -53,8 +54,8 @@ 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
# Set this to '1' if you don't want Cal to collect anonymous usage
CALCOM_TELEMETRY_DISABLED=
# ApiKey for cronjobs
CRON_API_KEY='0cc0e6c35519bba620c9360cfe3e68d0'
@@ -76,7 +77,6 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=
# 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=
+2 -4
View File
@@ -1,7 +1,5 @@
name: E2E test - embed
name: E2E test
on:
push:
branches: [ tests/ci-embed ]
pull_request_target: # So we can test on forks
branches:
- main
@@ -18,7 +16,7 @@ on:
jobs:
test:
timeout-minutes: 20
name: Testing Embeds
name: Embed and booking flow(for non-embed as well)
strategy:
matrix:
node: ["14.x"]
+2 -1
View File
@@ -7,6 +7,7 @@
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
"ban.spellright", // Spell check for docs
"stripe.vscode-stripe", // stripe VSCode extension
"Prisma.prisma" // syntax|format|completion for prisma
"Prisma.prisma", // syntax|format|completion for prisma
"rebornix.project-snippets" // Share useful snippets between collaborators
]
}
@@ -0,0 +1,76 @@
import React from "react";
import Select from "react-select";
import { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
import { trpc } from "@lib/trpc";
interface AdditionalCalendarSelectorProps {
isLoading?: boolean;
}
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
const { data } = optionProps;
return (
<InstallAppButton
type={data.type}
render={(installProps) => {
return (
<Button {...installProps} className="w-full" color="minimal">
{/* eslint-disable @next/next/no-img-element */}
{data.image && (
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
)}
<p>{data.label}</p>
</Button>
);
}}
/>
);
};
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations", { variant: "calendar", onlyInstalled: true }]);
return (
<QueryCell
query={query}
success={({ data }) => {
const options = data.items.map((item) => ({
label: item.name,
slug: item.slug,
image: item.imageSrc,
type: item.type,
}));
return (
<Select
name={"additionalCalendar"}
placeholder={t("connect_additional_calendar")}
options={options}
styles={{
placeholder: (defaultStyles) => {
return {
...defaultStyles,
color: "#3E3E3E",
marginLeft: "3px",
};
},
}}
isSearchable={false}
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 font-medium text-gray-700 sm:text-sm"
isLoading={isLoading}
components={{ Option: ImageOption }}
/>
);
}}
/>
);
};
export default AdditionalCalendarSelector;
+38 -15
View File
@@ -5,6 +5,8 @@ import {
FlagIcon,
MailIcon,
ShieldCheckIcon,
PlusIcon,
CheckIcon,
} from "@heroicons/react/outline";
import { ChevronLeftIcon } from "@heroicons/react/solid";
import Link from "next/link";
@@ -13,7 +15,7 @@ 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 { Button, SkeletonButton } from "@calcom/ui";
import Shell from "@components/Shell";
import Badge from "@components/ui/Badge";
@@ -59,7 +61,8 @@ export default function App({
currency: "USD",
useGrouping: false,
}).format(price);
const [installedApp, setInstalledApp] = useState(false);
const [installedApp, setInstalledApp] = useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function getInstalledApp(appCredentialType: string) {
const queryParam = new URLSearchParams();
@@ -70,9 +73,13 @@ export default function App({
headers: {
"Content-Type": "application/json",
},
}).then((data) => {
setIsLoading(false);
return data;
});
if (result.status === 200) {
setInstalledApp(true);
const res = await result.json();
setInstalledApp(res.count);
}
} catch (error) {
if (error instanceof Error) {
@@ -107,19 +114,35 @@ export default function App({
</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")}
{!isLoading ? (
isGlobal || installedApp > 0 ? (
<div className="space-x-3">
<Button StartIcon={CheckIcon} color="secondary" disabled>
{installedApp > 0
? t("active_install", { count: installedApp })
: t("globally_install")}
</Button>
)}
/>
<InstallAppButton
type={type}
render={(buttonProps) => (
<Button StartIcon={PlusIcon} data-testid="install-app-button" {...buttonProps}>
{t("add_another")}
</Button>
)}
/>
</div>
) : (
<InstallAppButton
type={type}
render={(buttonProps) => (
<Button data-testid="install-app-button" {...buttonProps}>
{t("install_app")}
</Button>
)}
/>
)
) : (
<SkeletonButton width="24" height="10" />
)}
{price !== 0 && (
<small className="block text-right">
@@ -1,9 +1,9 @@
import classNames from "classnames";
import React, { useEffect, useState } from "react";
import Select from "react-select";
import Button from "@calcom/ui/Button";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
interface Props {
@@ -24,6 +24,22 @@ const DestinationCalendarSelector = ({
const query = trpc.useQuery(["viewer.connectedCalendars"]);
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
// Extra styles to show prefixed text in react-select
const content = (hidePlaceholder = false) => {
if (!hidePlaceholder) {
return {
alignItems: "center",
display: "flex",
":before": {
content: `'${t("select_destination_calendar")}:'`,
display: "block",
marginRight: 8,
},
};
}
return {};
};
useEffect(() => {
const selected = query.data?.connectedCalendars
.map((connected) => connected.calendars ?? [])
@@ -52,23 +68,29 @@ const DestinationCalendarSelector = ({
})) ?? [];
return (
<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 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>
)}
<Select
name={"primarySelectedCalendar"}
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
options={options}
styles={{
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
option: (defaultStyles, state) => ({
...defaultStyles,
backgroundColor: state.isSelected
? state.isFocused
? "var(--brand-color)"
: "var(--brand-color)"
: state.isFocused
? "var(--brand-color-dark-mode)"
: "var(--brand-text-color)",
}),
}}
isSearchable={false}
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
className={classNames(
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm",
!hidePlaceholder && "font-medium"
)}
onChange={(option) => {
setSelectedOption(option);
if (!option) {
-27
View File
@@ -1,27 +0,0 @@
import React from "react";
import { SVGComponent } from "@lib/types/SVGComponent";
export default function EmptyScreen({
Icon,
headline,
description,
}: {
Icon: SVGComponent;
headline: string;
description: string | React.ReactElement;
}) {
return (
<>
<div className="min-h-80 my-6 flex flex-col items-center justify-center rounded-sm border border-dashed">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{headline}</h2>
<p className="text-sm leading-6 text-gray-600 dark:text-gray-300">{description}</p>
</div>
</div>
</>
);
}
+16 -4
View File
@@ -1,4 +1,11 @@
import { CreditCardIcon, KeyIcon, LockClosedIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
import {
CreditCardIcon,
KeyIcon,
LockClosedIcon,
UserGroupIcon,
UserIcon,
CodeIcon,
} from "@heroicons/react/solid";
import React, { ComponentProps } from "react";
import ErrorBoundary from "@lib/ErrorBoundary";
@@ -12,15 +19,20 @@ const tabs = [
href: "/settings/profile",
icon: UserIcon,
},
{
name: "teams",
href: "/settings/teams",
icon: UserGroupIcon,
},
{
name: "security",
href: "/settings/security",
icon: KeyIcon,
},
{
name: "teams",
href: "/settings/teams",
icon: UserGroupIcon,
name: "developer",
href: "/settings/developer",
icon: CodeIcon,
},
{
name: "billing",
+1 -10
View File
@@ -37,7 +37,6 @@ import classNames from "@lib/classNames";
import { WEBAPP_URL } from "@lib/config/constants";
import { shouldShowOnboarding } from "@lib/getting-started";
import useMeQuery from "@lib/hooks/useMeQuery";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding";
@@ -415,16 +414,8 @@ type LayoutProps = {
};
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;
@@ -509,7 +500,7 @@ function UserDropdown({ small }: { small?: boolean }) {
</DropdownMenuTrigger>
<DropdownMenuContent portalled={true}>
{helpOpen ? (
<HelpMenuItem />
<HelpMenuItem closeHelp={() => setHelpOpen(false)} />
) : (
<>
<DropdownMenuItem>
+2 -2
View File
@@ -4,10 +4,10 @@ import { SkeletonText } from "@calcom/ui";
import { ShellSubHeading } from "@components/Shell";
function SkeletonLoader() {
function SkeletonLoader({ className }: { className?: string }) {
return (
<>
<ShellSubHeading title={<div className="h-6 w-32 rounded-sm bg-gray-100"></div>} />
<ShellSubHeading title={<div className="h-6 w-32 rounded-sm bg-gray-100"></div>} {...{ className }} />
<ul className="-mx-4 animate-pulse divide-y divide-neutral-200 rounded-sm border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
+1 -3
View File
@@ -40,9 +40,7 @@ export default function SAMLLogin(props: Props) {
event.preventDefault();
// track Google logins. Without personal data/payload
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.googleLogin, collectPageParameters())
);
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
if (!props.hostedCal) {
await signIn("saml", {}, { tenant: props.samlTenantID, product: props.samlProductID });
@@ -62,7 +62,8 @@ const useOptions = () => {
const filter = useCallback(
({ offset, limit, current }: { offset?: ConfigType; limit?: ConfigType; current?: ConfigType }) => {
if (current) {
setFilteredOptions([options.find((option) => option.value === dayjs(current).toDate().valueOf())!]);
const currentOption = options.find((option) => option.value === dayjs(current).toDate().valueOf());
if (currentOption) setFilteredOptions([currentOption]);
} else
setFilteredOptions(
options.filter((option) => {
+26 -23
View File
@@ -25,6 +25,7 @@ import { TextArea } from "@calcom/ui/form/fields";
import { HttpError } from "@lib/core/http/error";
import useMeQuery from "@lib/hooks/useMeQuery";
import { linkValueToString } from "@lib/linkValueToString";
import { LocationType } from "@lib/location";
import { parseRecurringDates } from "@lib/parseDate";
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
@@ -86,6 +87,9 @@ function BookingListItem(booking: BookingItemProps) {
);
const isUpcoming = new Date(booking.endTime) >= new Date();
const isCancelled = booking.status === BookingStatus.CANCELLED;
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
const isRejected = booking.status === BookingStatus.REJECTED;
const isPending = booking.status === BookingStatus.PENDING;
const pendingActions: ActionType[] = [
{
@@ -199,25 +203,34 @@ function BookingListItem(booking: BookingItemProps) {
);
}
let location = booking.location || "";
if (location.includes("integration")) {
if (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) {
location = t("web_conference");
} else if (isConfirmed) {
location = linkValueToString(booking.location, t);
} else {
location = t("web_conferencing_details_to_follow");
}
}
const onClick = () => {
router.push({
pathname: "/success",
query: {
date: booking.startTime,
type: booking.eventType.id,
// TODO: Booking when fetched should have id 0 already(for Dynamic Events).
type: booking.eventType.id || 0,
eventSlug: booking.eventType.slug,
user: user?.username || "",
name: booking.attendees[0] ? booking.attendees[0].name : undefined,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
location: booking.location
? booking.location.includes("integration")
? (t("web_conferencing_details_to_follow") as string)
: booking.location
: "",
location: location,
eventName: booking.eventType.eventName || "",
bookingId: booking.id,
recur: booking.recurringEventId,
reschedule: booking.confirmed,
reschedule: isConfirmed,
listingStatus: booking.listingStatus,
status: booking.status,
},
@@ -312,14 +325,10 @@ function BookingListItem(booking: BookingItemProps) {
</div>
</div>
</td>
<td
className={"flex-1 ltr:pl-4 rtl:pr-4" + (booking.rejected ? " line-through" : "")}
onClick={onClick}>
<td className={"flex-1 ltr:pl-4 rtl:pr-4" + (isRejected ? " line-through" : "")} onClick={onClick}>
<div className="cursor-pointer py-4">
<div className="sm:hidden">
{!booking.confirmed && !booking.rejected && (
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>
)}
{isPending && <Tag className="mb-2 ltr:mr-2 rtl:ml-2">{t("unconfirmed")}</Tag>}
{!!booking?.eventType?.price && !booking.paid && (
<Tag className="mb-2 ltr:mr-2 rtl:ml-2">Pending payment</Tag>
)}
@@ -341,9 +350,7 @@ function BookingListItem(booking: BookingItemProps) {
{!!booking?.eventType?.price && !booking.paid && (
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
)}
{!booking.confirmed && !booking.rejected && (
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag>
)}
{isPending && <Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">{t("unconfirmed")}</Tag>}
</div>
{booking.description && (
<div
@@ -372,13 +379,9 @@ function BookingListItem(booking: BookingItemProps) {
<td className="whitespace-nowrap py-4 text-right text-sm font-medium ltr:pr-4 rtl:pl-4">
{isUpcoming && !isCancelled ? (
<>
{!booking.confirmed && !booking.rejected && user!.id === booking.user!.id && (
<TableActions actions={pendingActions} />
)}
{booking.confirmed && !booking.rejected && <TableActions actions={bookedActions} />}
{!booking.confirmed && booking.rejected && (
<div className="text-sm text-gray-500">{t("rejected")}</div>
)}
{isPending && user?.id === booking.user?.id && <TableActions actions={pendingActions} />}
{isConfirmed && <TableActions actions={bookedActions} />}
{isRejected && <div className="text-sm text-gray-500">{t("rejected")}</div>}
</>
) : null}
{isCancelled && booking.rescheduled && (
@@ -78,9 +78,7 @@ export default function CancelBooking(props: Props) {
reason: cancellationReason,
};
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters())
);
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
const res = await fetch("/api/cancel", {
body: JSON.stringify(payload),
@@ -143,12 +143,13 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
useEffect(() => {
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
telemetry.withJitsu((jitsu) =>
jitsu.track(
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
)
);
);
}
}, [telemetry]);
const changeDate = useCallback(
@@ -252,62 +253,64 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
<h1 className="text-bookingdark mb-4 break-words 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>
)}
{eventType.locations.length === 1 && (
<p className="text-bookinglight mb-2 dark:text-white">
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{locationKeyToString(eventType.locations[0], t)}
</p>
)}
{eventType.locations.length === 1 && (
<p className="text-bookinglight mb-2 dark:text-white">
{Object.values(AppStoreLocationType).includes(
eventType.locations[0].type as unknown as AppStoreLocationType
) ? (
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
) : (
<div className="flex flex-col space-y-4">
{eventType?.description && (
<p className="text-gray-600 dark:text-white">
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
{eventType.description}
</p>
)}
{eventType.locations.length === 1 && (
<p className="text-gray-600 dark:text-white">
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
)}
{locationKeyToString(eventType.locations[0], t)}
</p>
)}
{eventType.locations.length === 1 && (
<p className="text-gray-600 dark:text-white">
{Object.values(AppStoreLocationType).includes(
eventType.locations[0].type as unknown as AppStoreLocationType
) ? (
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
) : (
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
)}
{locationKeyToString(eventType.locations[0], t)}
{locationKeyToString(eventType.locations[0], t)}
</p>
)}
<p className="text-gray-600 dark:text-white">
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
{eventType.length} {t("minutes")}
</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")}
</p>
{eventType.price > 0 && (
<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}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</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>
{eventType.price > 0 && (
<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}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</IntlProvider>
</div>
)}
<div className="md:hidden">
{booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-8 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>
@@ -343,88 +346,93 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
<h1 className="font-cal mb-4 break-words text-xl font-semibold text-gray-900 dark:text-white">
{eventType.title}
</h1>
{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>
)}
{eventType.locations.length === 1 && (
<p className="text-bookinglight mb-2 dark:text-white">
{Object.values(AppStoreLocationType).includes(
eventType.locations[0].type as unknown as AppStoreLocationType
) ? (
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
) : (
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
)}
{locationKeyToString(eventType.locations[0], t)}
</p>
)}
{eventType.locations.length > 1 && (
<div className="text-bookinglight flex-warp mb-2 flex dark:text-white">
<div className="mr-[10px] ml-[2px] -mt-1 ">
<LocationMarkerIcon className="inline-block h-4 w-4 text-gray-400" />
<div className="flex flex-col space-y-4">
{eventType?.description && (
<div className="flex text-gray-600 dark:text-white">
<div>
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
</div>
<p>{eventType.description}</p>
</div>
<p>
{eventType.locations.map((el, i, arr) => {
return (
<span key={el.type}>
{locationKeyToString(el, t)}{" "}
{arr.length - 1 !== i && (
<span className="font-light"> {t("or_lowercase")} </span>
)}
</span>
);
})}
)}
{eventType.locations.length === 1 && (
<p className="text-gray-600 dark:text-white">
{Object.values(AppStoreLocationType).includes(
eventType.locations[0].type as unknown as AppStoreLocationType
) ? (
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
) : (
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
)}
{locationKeyToString(eventType.locations[0], t)}
</p>
</div>
)}
<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-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}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</IntlProvider>
)}
{eventType.locations.length > 1 && (
<div className="flex-warp flex text-gray-600 dark:text-white">
<div className="mr-[10px] ml-[2px] -mt-1 ">
<LocationMarkerIcon className="inline-block h-4 w-4 text-gray-400" />
</div>
<p>
{eventType.locations.map((el, i, arr) => {
return (
<span key={el.type}>
{locationKeyToString(el, t)}{" "}
{arr.length - 1 !== i && (
<span className="font-light"> {t("or_lowercase")} </span>
)}
</span>
);
})}
</p>
</div>
)}
<p className="text-gray-600 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>
)}
<TimezoneDropdown />
{!rescheduleUid && eventType.recurringEvent?.count && eventType.recurringEvent?.freq && (
<div className="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-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
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="-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}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</IntlProvider>
</p>
)}
<TimezoneDropdown />
</div>
{previousPage === `${WEBAPP_URL}/${profile.slug}` && (
<div className="flex h-full flex-col justify-end">
<ArrowLeftIcon
@@ -496,7 +504,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
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-600 dark:text-white">
<Collapsible.Trigger className="min-w-32 text-bookinglight mb-1 -ml-2 px-2 py-1 text-left dark:text-white">
<GlobeIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{timeZone()}
{isTimeOptionsOpen ? (
@@ -70,7 +70,9 @@ const PhoneInput = dynamic(
() => import("@components/ui/form/PhoneInput")
) as unknown as typeof PhoneInputType;
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
type BookingPageProps = (BookPageProps | TeamBookingPageProps | HashLinkPageProps) & {
locationLabels: Record<LocationType, string>;
};
type BookingFormValues = {
name: string;
@@ -83,6 +85,7 @@ type BookingFormValues = {
customInputs?: {
[key: string]: string | boolean;
};
rescheduleReason?: string;
};
const BookingPage = ({
@@ -106,12 +109,13 @@ const BookingPage = ({
const telemetry = useTelemetry();
useEffect(() => {
telemetry.withJitsu((jitsu) =>
jitsu.track(
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
)
);
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -255,6 +259,7 @@ const BookingPage = ({
email: primaryAttendee.email || "",
guests: guestListEmails,
notes: booking.description || "",
rescheduleReason: "",
customInputs: eventType.customInputs.reduce(
(customInputs, input) => ({
...customInputs,
@@ -330,13 +335,10 @@ const BookingPage = ({
}
const bookEvent = (booking: BookingFormValues) => {
telemetry.withJitsu((jitsu) =>
jitsu.track(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
)
telemetry.event(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
{ isTeamBooking: document.URL.includes("team/") }
);
// "metadata" is a reserved key to allow for connecting external users without relying on the email address.
// <...url>&metadata[user_id]=123 will be send as a custom input field as the hidden type.
@@ -786,23 +788,36 @@ const BookingPage = ({
<label
htmlFor="notes"
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
{t("additional_notes")}
{rescheduleUid ? t("reschedule_optional") : t("additional_notes")}
</label>
<textarea
{...bookingForm.register("notes")}
id="notes"
name="notes"
rows={3}
className={inputClassName}
placeholder={t("share_additional_notes")}
disabled={disabledExceptForOwner}
/>
{rescheduleUid ? (
<textarea
{...bookingForm.register("rescheduleReason")}
id="rescheduleReason"
name="rescheduleReason"
rows={3}
className={inputClassName}
placeholder={t("reschedule_placeholder")}
disabled={disabledExceptForOwner}
/>
) : (
<textarea
{...bookingForm.register("notes")}
id="notes"
name="notes"
rows={3}
className={inputClassName}
placeholder={t("share_additional_notes")}
disabled={disabledExceptForOwner}
/>
)}
</div>
<div className="flex items-start space-x-2 rtl:space-x-reverse">
<Button
type="submit"
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
loading={mutation.isLoading}>
loading={mutation.isLoading || recurringMutation.isLoading}>
{rescheduleUid ? t("reschedule") : t("confirm")}
</Button>
<Button color="secondary" type="button" onClick={() => router.back()}>
@@ -1,24 +1,23 @@
import { ClockIcon, CreditCardIcon, RefreshIcon, UserIcon, UsersIcon } from "@heroicons/react/solid";
import { SchedulingType } from "@prisma/client";
import { Prisma } from "@prisma/client";
import React, { useMemo } from "react";
import {
ClipboardCheckIcon,
ClockIcon,
CreditCardIcon,
RefreshIcon,
UserIcon,
UsersIcon,
} from "@heroicons/react/solid";
import { Prisma, SchedulingType } from "@prisma/client";
import { useMemo } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { RecurringEvent } from "@calcom/types/Calendar";
import classNames from "@lib/classNames";
const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
select: {
id: true,
length: true,
price: true,
currency: true,
schedulingType: true,
recurringEvent: true,
description: true,
},
select: baseEventTypeSelect,
});
type EventType = Prisma.EventTypeGetPayload<typeof eventTypeData>;
@@ -38,39 +37,42 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
return (
<>
<div className={classNames("text-neutral-500 dark:text-white", className)}>
<div
className={classNames("flex flex-wrap text-neutral-500 dark:text-white sm:flex-nowrap", className)}>
{eventType.description && (
<h2 className="max-w-[280px] overflow-hidden text-ellipsis opacity-60 sm:max-w-[500px]">
{eventType.description.substring(0, 100)}
{eventType.description.length > 100 && "..."}
</h2>
)}
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex whitespace-nowrap">
<ClockIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
<ul className="mt-2 flex flex-wrap sm:flex-nowrap">
<li className="mr-4 flex items-center whitespace-nowrap">
<ClockIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
{eventType.length}m
</li>
{eventType.schedulingType ? (
<li className="flex whitespace-nowrap">
<UsersIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
<li className="mr-4 flex items-center whitespace-nowrap">
<UsersIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
</li>
) : (
<li className="flex whitespace-nowrap">
<UserIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
<li className="mr-4 flex items-center whitespace-nowrap">
<UserIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
{t("1_on_1")}
</li>
)}
</ul>
<ul className="mt-2 flex">
{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" />
<li className="mr-4 flex items-center whitespace-nowrap">
<RefreshIcon className="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" />
<li className="mr-4 flex items-center whitespace-nowrap">
<CreditCardIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
@@ -80,6 +82,12 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
</IntlProvider>
</li>
)}
{eventType.requiresConfirmation && (
<li className="mr-4 flex items-center whitespace-nowrap">
<ClipboardCheckIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
Opt-in
</li>
)}
</ul>
</div>
</>
@@ -1,19 +1,20 @@
import { Fragment } from "react";
import { useMutation } from "react-query";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
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 { trpc } from "@lib/trpc";
import AdditionalCalendarSelector from "@components/AdditionalCalendarSelector";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
import { List } from "@components/List";
import { ShellSubHeading } from "@components/Shell";
import SkeletonLoader from "@components/apps/SkeletonLoader";
import DisconnectIntegration from "./DisconnectIntegration";
import IntegrationListItem from "./IntegrationListItem";
@@ -163,40 +164,6 @@ function ConnectedCalendarsList(props: Props) {
);
}
function CalendarList(props: Props) {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations"]);
return (
<QueryCell
query={query}
success={({ data }) => (
<List>
{data.calendar.items.map((item) => (
<IntegrationListItem
key={item.title}
title={item.title}
imageSrc={item.imageSrc}
description={item.description}
actions={
<InstallAppButton
type={item.type}
render={(buttonProps) => (
<Button color="secondary" {...buttonProps}>
{t("connect")}
</Button>
)}
onChanged={() => props.onChanged()}
/>
}
/>
))}
</List>
)}
/>
);
}
export function CalendarListContainer(props: { heading?: false }) {
const { t } = useLocale();
const { heading = true } = props;
@@ -207,39 +174,54 @@ export function CalendarListContainer(props: { heading?: false }) {
utils.invalidateQueries(["viewer.connectedCalendars"]),
]);
const query = trpc.useQuery(["viewer.connectedCalendars"]);
const installedCalendars = trpc.useQuery([
"viewer.integrations",
{ variant: "calendar", onlyInstalled: true },
]);
const mutation = trpc.useMutation("viewer.setDestinationCalendar");
return (
<>
{heading && (
<ShellSubHeading
className="mt-10 mb-0"
title={
<SubHeadingTitleWithConnections
title="Calendars"
numConnections={query.data?.connectedCalendars.length}
/>
}
subtitle={t("configure_how_your_event_types_interact")}
actions={
<div className="sm:min-w-80 block max-w-full">
<DestinationCalendarSelector
onChange={mutation.mutate}
isLoading={mutation.isLoading}
value={query.data?.destinationCalendar?.externalId}
/>
</div>
}
/>
)}
<ConnectedCalendarsList onChanged={onChanged} />
{!!query.data?.connectedCalendars.length && (
<ShellSubHeading
className="mt-6"
title={<SubHeadingTitleWithConnections title={t("connect_an_additional_calendar")} />}
/>
)}
<CalendarList onChanged={onChanged} />
</>
<QueryCell
query={query}
customLoader={<SkeletonLoader className="mt-10" />}
success={({ data }) => {
return (
<>
{(!!data.connectedCalendars.length || !!installedCalendars.data?.items.length) && (
<>
{heading && (
<ShellSubHeading
className="mt-10 mb-0"
title={
<SubHeadingTitleWithConnections
title="Calendars"
numConnections={data.connectedCalendars.length}
/>
}
subtitle={t("configure_how_your_event_types_interact")}
actions={
<div className="flex flex-col xl:flex-row xl:space-x-5">
<div className="sm:min-w-80 block max-w-full">
<DestinationCalendarSelector
onChange={mutation.mutate}
isLoading={mutation.isLoading}
value={data.destinationCalendar?.externalId}
/>
</div>
{!!data.connectedCalendars.length && (
<div className="sm:min-w-80 inline max-w-full">
<AdditionalCalendarSelector isLoading={mutation.isLoading} />
</div>
)}
</div>
}
/>
)}
<ConnectedCalendarsList onChanged={onChanged} />
</>
)}
</>
);
}}
/>
);
}
@@ -1,6 +1,7 @@
import { useState } from "react";
import { useMutation } from "react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { ButtonBaseProps } from "@calcom/ui/Button";
import { Dialog } from "@calcom/ui/Dialog";
@@ -13,6 +14,7 @@ export default function DisconnectIntegration(props: {
render: (renderProps: ButtonBaseProps) => JSX.Element;
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
}) {
const { t } = useLocale();
const [modalOpen, setModalOpen] = useState(false);
const mutation = useMutation(
async () => {
@@ -43,13 +45,13 @@ export default function DisconnectIntegration(props: {
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<ConfirmationDialogContent
variety="danger"
title="Disconnect Integration"
confirmBtnText="Yes, disconnect integration"
title={t("remove_app")}
confirmBtnText={t("yes_remove_app")}
cancelBtnText="Cancel"
onConfirm={() => {
mutation.mutate();
}}>
Are you sure you want to disconnect this integration?
{t("are_you_sure_you_want_to_remove_this_app")}
</ConfirmationDialogContent>
</Dialog>
{props.render({
@@ -35,7 +35,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
defaultValues,
});
const selectedInputType = useWatch({ name: "type", control });
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value)!;
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value);
const onCancel = () => {
props.onCancel();
@@ -1,6 +1,5 @@
import { MembershipRole } from "@prisma/client";
import { useState } from "react";
import React, { SyntheticEvent, useEffect } from "react";
import { SyntheticEvent, useMemo, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
@@ -11,12 +10,10 @@ import ModalContainer from "@components/ui/ModalContainer";
import Select from "@components/ui/form/Select";
type MembershipRoleOption = {
label: string;
value: MembershipRole;
label?: string;
};
const options: MembershipRoleOption[] = [{ value: "MEMBER" }, { value: "ADMIN" }, { value: "OWNER" }];
export default function MemberChangeRoleModal(props: {
isOpen: boolean;
currentMember: MembershipRole;
@@ -25,18 +22,32 @@ export default function MemberChangeRoleModal(props: {
initialRole: MembershipRole;
onExit: () => void;
}) {
useEffect(() => {
options.forEach((option, i) => {
options[i].label = t(option.value.toLowerCase());
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { t } = useLocale();
const [role, setRole] = useState(
options.find((option) => option.value === props.initialRole || MembershipRole.MEMBER)!
const options = useMemo(() => {
return [
{
label: t("member"),
value: MembershipRole.MEMBER,
},
{
label: t("admin"),
value: MembershipRole.ADMIN,
},
{
label: t("owner"),
value: MembershipRole.OWNER,
},
].filter(({ value }) => value !== MembershipRole.OWNER || props.currentMember === MembershipRole.OWNER);
}, [t, props.currentMember]);
const [role, setRole] = useState<MembershipRoleOption>(
options.find((option) => option.value === props.initialRole) || {
label: t("member"),
value: MembershipRole.MEMBER,
}
);
const [errorMessage, setErrorMessage] = useState("");
const { t } = useLocale();
const utils = trpc.useContext();
const changeRoleMutation = trpc.useMutation("viewer.teams.changeMemberRole", {
@@ -76,7 +87,7 @@ export default function MemberChangeRoleModal(props: {
{/*<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}
options={options}
value={role}
onChange={(option) => option && setRole(option)}
id="role"
+10 -10
View File
@@ -1,17 +1,17 @@
import classNames from "classnames";
import React from "react";
import React, { PropsWithChildren } from "react";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
interface Props extends React.PropsWithChildren<any> {
wide?: boolean;
scroll?: boolean;
noPadding?: boolean;
isOpen: boolean;
onExit: () => void;
}
export default function ModalContainer(props: Props) {
export default function ModalContainer(
props: PropsWithChildren<{
wide?: boolean;
scroll?: boolean;
noPadding?: boolean;
isOpen: boolean;
onExit: () => void;
}>
) {
return (
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<Dialog open={props.isOpen} onOpenChange={props.onExit}>
@@ -1,16 +1,15 @@
import classNames from "classnames";
import Image from "next/image";
import { PlusIcon } from "@heroicons/react/solid";
import { useState } from "react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { QueryCell } from "@lib/QueryCell";
import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
import { List } from "@components/List";
import { ShellSubHeading } from "@components/Shell";
import SkeletonLoader from "@components/apps/SkeletonLoader";
import WebhookDialogForm from "@components/webhook/WebhookDialogForm";
import WebhookListItem, { TWebhook } from "@components/webhook/WebhookListItem";
@@ -21,47 +20,35 @@ export type WebhookListContainerType = {
};
export default function WebhookListContainer(props: WebhookListContainerType) {
const { t } = useLocale();
const query = props.eventTypeId
? trpc.useQuery(["viewer.webhook.list", { eventTypeId: props.eventTypeId }], {
suspense: true,
})
: trpc.useQuery(["viewer.webhook.list"], {
suspense: true,
});
const query = trpc.useQuery(["viewer.webhook.list", { eventTypeId: props.eventTypeId }], {
suspense: true,
});
const [newWebhookModal, setNewWebhookModal] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [editing, setEditing] = useState<TWebhook | null>(null);
return (
<QueryCell
query={query}
customLoader={<SkeletonLoader />}
success={({ data }) => (
<>
<ShellSubHeading className="mt-10" title={props.title} subtitle={props.subtitle} />
<List>
<ListItem 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="/apps/webhooks.svg" alt="Webhooks" />
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">Webhooks</ListItemTitle>
<ListItemText component="p">{t("automation")}</ListItemText>
</div>
<div>
<Button
color="secondary"
onClick={() => setNewWebhookModal(true)}
data-testid="new_webhook">
{t("new_webhook")}
</Button>
</div>
</div>
</ListItem>
</List>
<div className="border-b border-gray-200 py-8 pl-2 pr-1">
<ShellSubHeading
className="mt-2"
title={props.title}
subtitle={props.subtitle}
actions={
<Button
color="secondary"
size="icon"
StartIcon={PlusIcon}
onClick={() => setNewWebhookModal(true)}
data-testid="new_webhook"
/>
}
/>
{data.length ? (
<List>
<List className="mt-6">
{data.map((item) => (
<WebhookListItem
key={item.id}
@@ -97,7 +84,7 @@ export default function WebhookListContainer(props: WebhookListContainerType) {
)}
</DialogContent>
</Dialog>
</>
</div>
)}
/>
);
@@ -23,14 +23,14 @@ export default function WebhookListItem(props: { webhook: TWebhook; onEditWebhoo
});
return (
<ListItem className="-mt-px flex w-full p-4">
<ListItem className={classNames("flex w-full p-4", props.webhook.active ? "bg-white" : "bg-gray-100")}>
<div className="flex w-full justify-between">
<div className="flex max-w-full flex-col truncate">
<div className="flex space-y-1">
<span
className={classNames(
"truncate text-sm",
props.webhook.active ? "text-neutral-700" : "text-neutral-200"
props.webhook.active ? "text-neutral-700" : "text-neutral-400"
)}>
{props.webhook.subscriberUrl}
</span>
@@ -41,8 +41,8 @@ export default function WebhookListItem(props: { webhook: TWebhook; onEditWebhoo
<span
key={ind}
className={classNames(
"w-max rounded-sm px-1 text-xs ",
props.webhook.active ? "bg-blue-100 text-blue-700" : "bg-blue-50 text-blue-200"
"w-max rounded-sm px-1 text-xs",
props.webhook.active ? "text-grey-200 bg-gray-200" : "bg-grey-50 text-neutral-400"
)}>
{t(`${eventTrigger.toLowerCase()}`)}
</span>
+6 -3
View File
@@ -3,8 +3,7 @@ import { useSession } from "next-auth/react";
import React, { AriaRole, ComponentType, FC, Fragment } from "react";
import { CONSOLE_URL } from "@calcom/lib/constants";
import EmptyScreen from "@components/EmptyScreen";
import EmptyScreen from "@calcom/ui/EmptyScreen";
type LicenseRequiredProps = {
as?: keyof JSX.IntrinsicElements | "";
@@ -34,7 +33,11 @@ const LicenseRequired: FC<LicenseRequiredProps> = ({ children, as = "", ...rest
<a href={CONSOLE_URL} target="_blank" rel="noopener noreferrer" className="underline">
Cal.com console
</a>
.
. If your team already has a license, please contact{" "}
<a href="mailto:peer@cal.com" className="underline">
peer@cal.com
</a>{" "}
for help.
</>
}
/>
@@ -11,6 +11,8 @@ import { QueryCell } from "@lib/QueryCell";
import { trpc } from "@lib/trpc";
import { List } from "@components/List";
import { ShellSubHeading } from "@components/Shell";
import SkeletonLoader from "@components/apps/SkeletonLoader";
import LicenseRequired from "../LicenseRequired";
@@ -22,25 +24,28 @@ function ApiKeyListContainer() {
const [editModalOpen, setEditModalOpen] = useState(false);
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | null>(null);
return (
<>
<div className="flex flex-col justify-between truncate pl-2 pr-1 sm:flex-row">
<div className="mt-9">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
</div>
<div className="mb-9 sm:self-center">
<Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}>
{t("generate_new_api_key")}
</Button>
</div>
</div>
<div className="border-b border-gray-200 py-8 pl-2 pr-1">
<ShellSubHeading
className="mt-2"
title={t("api_keys")}
subtitle={t("api_keys_subtitle")}
actions={
<Button
color="secondary"
size="icon"
StartIcon={PlusIcon}
onClick={() => setNewApiKeyModal(true)}
/>
}
/>
<LicenseRequired>
<QueryCell
query={query}
customLoader={<SkeletonLoader />}
success={({ data }) => (
<>
{data.length > 0 && (
<List className="pb-6">
<List className="mt-6">
{data.map((item) => (
<ApiKeyListItem
key={item.id}
@@ -80,7 +85,7 @@ function ApiKeyListContainer() {
)}
/>
</LicenseRequired>
</>
</div>
);
}
@@ -30,7 +30,7 @@ export default function ApiKeyListItem(props: { apiKey: TApiKeys; onEditApiKey:
},
});
return (
<ListItem className="-mt-px flex w-full p-4">
<ListItem className="flex w-full p-4">
<div className="flex w-full justify-between">
<div className="flex max-w-full flex-col truncate">
<div className="flex space-x-2">
@@ -74,7 +74,7 @@ export default function SAMLConfiguration({
const rawMetadata = samlConfigRef.current.value;
// track Google logins. Without personal data/payload
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.samlConfig, collectPageParameters()));
telemetry.event(telemetryEventTypes.samlConfig, collectPageParameters());
mutation.mutate({
encodedRawMetadata: Buffer.from(rawMetadata).toString("base64"),
@@ -10,14 +10,23 @@ import { trpc } from "@lib/trpc";
import ContactMenuItem from "./ContactMenuItem";
export default function HelpMenuItem() {
interface HelpMenuItemProps {
closeHelp: () => void;
}
export default function HelpMenuItem({ closeHelp }: HelpMenuItemProps) {
const [rating, setRating] = useState<null | string>(null);
const [comment, setComment] = useState("");
// const [errorMessage, setErrorMessage] = useState(false);
const [disableSubmit, setDisableSubmit] = useState(true);
const { t } = useLocale();
const mutation = trpc.useMutation("viewer.submitFeedback");
const mutation = trpc.useMutation("viewer.submitFeedback", {
onSuccess: () => {
setDisableSubmit(true);
showToast("Thank you, feedback submitted", "success");
closeHelp();
},
});
const onRatingClick = (value: string) => {
setRating(value);
@@ -26,11 +35,6 @@ export default function HelpMenuItem() {
const sendFeedback = async (rating: string, comment: string) => {
mutation.mutate({ rating: rating, comment: comment });
if (mutation.isSuccess) {
setDisableSubmit(true);
showToast("Thank you, feedback submitted", "success");
}
};
return (
@@ -1,17 +1,13 @@
import { ChatAltIcon } from "@heroicons/react/solid";
import { useState } from "react";
import { HelpScout, useChat } from "react-live-chat-loader";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { DropdownMenuItem } from "@calcom/ui/Dropdown";
import classNames from "@lib/classNames";
export default function HelpscoutMenuItem() {
const { t } = useLocale();
const [active, setActive] = useState(false);
const [state, loadChat] = useChat();
const [, loadChat] = useChat();
function handleClick() {
setActive(true);
+1 -1
View File
@@ -2,7 +2,7 @@ import { FC } from "react";
import { LiveChatLoaderProvider } from "react-live-chat-loader";
const Provider: FC<{ children: React.ReactNode }> = ({ children }) => (
<LiveChatLoaderProvider providerKey={process.env.NEXT_PUBLIC_HELPSCOUT_KEY!} provider="helpScout">
<LiveChatLoaderProvider providerKey={process.env.NEXT_PUBLIC_HELPSCOUT_KEY || ""} provider="helpScout">
<>{children}</>
</LiveChatLoaderProvider>
);
@@ -1,9 +1,4 @@
import { ChatAltIcon } from "@heroicons/react/solid";
import { DropdownMenuItem } from "@calcom/ui/Dropdown";
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useIntercom } from "./useIntercom";
+1 -1
View File
@@ -2,7 +2,7 @@ import { FC } from "react";
import { IntercomProvider } from "react-use-intercom";
const Provider: FC<{ children: React.ReactNode }> = ({ children }) => (
<IntercomProvider appId={process.env.NEXT_PUBLIC_INTERCOM_APP_ID!}>{children}</IntercomProvider>
<IntercomProvider appId={process.env.NEXT_PUBLIC_INTERCOM_APP_ID || ""}>{children}</IntercomProvider>
);
export default Provider;
+1 -8
View File
@@ -4,20 +4,13 @@ import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@calcom/emails";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
import { createPaymentLink } from "@calcom/stripe/client";
import stripe, { PaymentData } from "@calcom/stripe/server";
import { CalendarEvent } from "@calcom/types/Calendar";
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager";
export type PaymentInfo = {
link?: string | null;
reason?: string | null;
id?: string | null;
};
const stripeKeysSchema = z.object({
payment_fee_fixed: z.number(),
payment_fee_percentage: z.number(),
@@ -1,11 +1,7 @@
import { ChatAltIcon } from "@heroicons/react/solid";
import Script from "next/script";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { DropdownMenuItem } from "@calcom/ui/Dropdown";
import classNames from "@lib/classNames";
const ZENDESK_KEY = process.env.NEXT_PUBLIC_ZENDESK_KEY;
@@ -1,9 +1,10 @@
import { Prisma } from "@prisma/client";
import { BookingStatus, Prisma } from "@prisma/client";
import { buffer } from "micro";
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
import EventManager from "@calcom/core/EventManager";
import { sendScheduledEmails } from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
@@ -12,7 +13,6 @@ import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import { IS_PRODUCTION } from "@lib/config/constants";
import { HttpError as HttpCode } from "@lib/core/http/error";
import { sendScheduledEmails } from "@lib/emails/email-manager";
import { getTranslation } from "@server/lib/i18n";
@@ -44,13 +44,13 @@ async function handlePaymentSuccess(event: Stripe.Event) {
},
select: {
...bookingMinimalSelect,
confirmed: true,
location: true,
eventTypeId: true,
userId: true,
uid: true,
paid: true,
destinationCalendar: true,
status: true,
user: {
select: {
id: true,
@@ -125,10 +125,11 @@ async function handlePaymentSuccess(event: Stripe.Event) {
const bookingData: Prisma.BookingUpdateInput = {
paid: true,
confirmed: true,
status: BookingStatus.ACCEPTED,
};
if (booking.confirmed) {
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
if (isConfirmed) {
const eventManager = new EventManager(user);
const scheduleResult = await eventManager.create(evt);
bookingData.references = { create: scheduleResult.referencesToCreate };
+3 -3
View File
@@ -66,7 +66,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
});
if (!rawPayment) throw Error("Payment not found");
if (!rawPayment) return { notFound: true };
const { data, booking: _booking, ...restPayment } = rawPayment;
const payment = {
@@ -74,7 +74,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
data: data as unknown as PaymentData,
};
if (!_booking) throw Error("Booking not found");
if (!_booking) return { notFound: true };
const { startTime, eventType, ...restBooking } = _booking;
const booking = {
@@ -82,7 +82,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
startTime: startTime.toString(),
};
if (!eventType) throw Error("Event not found");
if (!eventType) return { notFound: true };
const [user] = eventType.users;
if (!user) return { notFound: true };
+8 -11
View File
@@ -7,7 +7,6 @@ import DynamicHelpscoutProvider from "@ee/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@ee/lib/intercom/providerDynamic";
import usePublicPage from "@lib/hooks/usePublicPage";
import { createTelemetryClient, TelemetryProvider } from "@lib/telemetry";
import { trpc } from "./trpc";
@@ -51,17 +50,15 @@ const AppProviders = (props: AppPropsWithChildren) => {
<CustomI18nextProvider {...props}>{props.children}</CustomI18nextProvider>
</SessionProvider>
);
const telemetryClient = useMemo(createTelemetryClient, []);
if (isPublicPage) {
return RemainingProviders;
}
return (
<TelemetryProvider value={telemetryClient}>
{isPublicPage ? (
RemainingProviders
) : (
<DynamicHelpscoutProvider>
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
</DynamicHelpscoutProvider>
)}
</TelemetryProvider>
<DynamicHelpscoutProvider>
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
</DynamicHelpscoutProvider>
);
};
@@ -1,173 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
linkIcon,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
return {
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.attendee.language.translate("awaiting_payment_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.attendee.language.translate("meeting_awaiting_payment")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.attendee.language.translate("awaiting_payment_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.attendee.language.translate("meeting_awaiting_payment"),
this.attendee.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
${this.getManageLink()}
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;"></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
protected getManageLink(): string {
const manageText = this.attendee.language.translate("pay_now");
if (this.calEvent.paymentInfo) {
return `
<tr>
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
<p style="display:inline-block;background:#292929;color:#ffffff;font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
<a style="color: #FFFFFF; text-decoration: none;" href="${
this.calEvent.paymentInfo.link
}" target="_blank">${manageText} <img src="${linkIcon()}" width="12px"></img></a>
</p>
</td>
</tr>
`;
}
return "";
}
}
@@ -1,144 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
return {
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.attendee.language.translate("event_cancelled_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.attendee.language.translate("event_request_cancelled")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.cancellationReason && this.getCancellationReason()}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.attendee.language.translate("event_cancelled_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("xCircle")}
${emailScheduledBodyHeaderContent(
this.attendee.language.translate("event_request_cancelled"),
this.attendee.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.cancellationReason && this.getCancellationReason()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
protected getCancellationReason(): string {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.attendee.language.translate("cancellation_reason")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p>
</div>`;
}
}
@@ -1,139 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
return {
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.attendee.language.translate("event_declined_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.attendee.language.translate(
this.recurringEvent?.count ? "event_request_declined_recurring" : "event_request_declined"
)}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.getRejectionReason()}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.attendee.language.translate("event_declined_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("xCircle")}
${emailScheduledBodyHeaderContent(
this.attendee.language.translate(
this.recurringEvent?.count ? "event_request_declined_recurring" : "event_request_declined"
),
this.attendee.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.getRejectionReason()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,165 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.attendee.language.translate("location_changed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
// Only the original attendee can make changes to the event
// Guests cannot
if (this.attendee === this.calEvent.attendees[0]) {
return `
${this.attendee.language.translate("event_location_changed")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.attendee.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
return `
${this.attendee.language.translate("event_location_changed")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getAdditionalNotes()}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.attendee.language.translate("location_changed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.attendee.language.translate("event_location_changed"),
this.attendee.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
${this.getManageLink()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,145 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.attendees[0].email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("booking_submitted_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.calEvent.attendees[0].language.translate("booking_submitted", {
name: this.calEvent.attendees[0].name,
})}
${this.calEvent.attendees[0].language.translate("user_needs_to_confirm_or_reject_booking", {
user: this.calEvent.organizer.name,
})}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.attendees[0].language.translate("booking_submitted_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate(
this.recurringEvent?.count ? "booking_submitted_recurring" : "booking_submitted"
),
this.calEvent.organizer.language.translate(
this.recurringEvent.count
? "user_needs_to_confirm_or_reject_booking_recurring"
: "user_needs_to_confirm_or_reject_booking",
{
user: this.calEvent.organizer.name,
}
)
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,210 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeRequestRescheduledEmail extends OrganizerScheduledEmail {
private metadata: { rescheduleLink: string };
constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }, recurringEvent: RecurringEvent) {
super(calEvent, recurringEvent);
this.metadata = metadata;
}
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.attendees[0].email];
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("requested_to_reschedule_subject_attendee", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
// @OVERRIDE
protected getiCalEventAsString(): string | undefined {
const icsEvent = createEvent({
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
title: this.calEvent.organizer.language.translate("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
}),
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
status: "CANCELLED",
method: "CANCEL",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
// @OVERRIDE
protected getWhen(): string {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("when")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;text-decoration: line-through;">
${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format(
"YYYY"
)} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)} <span style="color: #888888">(${this.getTimezone()})</span>
</p>
</div>`;
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("request_reschedule_title_attendee")}
${this.calEvent.organizer.language.translate("request_reschedule_subtitle", {
organizer: this.calEvent.organizer.name,
})},
${this.getWhat()}
${this.getWhen()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("request_reschedule_title_attendee"),
this.calEvent.organizer.language.translate("request_reschedule_subtitle", {
organizer: this.calEvent.organizer.name,
})
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;text-align:center;color:#3E3E3E;">
<a style="padding: 8px 16px;background-color: #292929;color: white;border-radius: 2px;display: inline-block;margin-bottom: 16px;"
href="${this.metadata.rescheduleLink}" target="_blank"
>
Book a new time
<img src="https://app.cal.com/emails/linkIcon.png" style="width:16px; margin-left: 5px;filter: brightness(0) invert(1); vertical-align: top;" />
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,168 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.attendee.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
// Only the original attendee can make changes to the event
// Guests cannot
if (this.attendee === this.calEvent.attendees[0]) {
return `
${this.attendee.language.translate("event_has_been_rescheduled")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.attendee.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
return `
${this.attendee.language.translate("event_has_been_rescheduled")}
${this.attendee.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.attendee.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.attendee.language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.attendee.language.translate("event_has_been_rescheduled"),
this.attendee.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
${this.getManageLink()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,455 +0,0 @@
import dayjs, { Dayjs } from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { createEvent, DateArray } from "ics";
import rrule from "rrule";
import { getAppName } from "@calcom/app-store/utils";
import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser";
import type { CalendarEvent, Person, RecurringEvent } from "@calcom/types/Calendar";
import BaseEmail from "@lib/emails/templates/_base-email";
import {
emailBodyLogo,
emailHead,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
emailSchedulingBodyHeader,
linkIcon,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class AttendeeScheduledEmail extends BaseEmail {
calEvent: CalendarEvent;
attendee: Person;
recurringEvent: RecurringEvent;
constructor(calEvent: CalendarEvent, attendee: Person, recurringEvent: RecurringEvent) {
super();
this.name = "SEND_BOOKING_CONFIRMATION";
this.calEvent = calEvent;
this.attendee = attendee;
this.recurringEvent = recurringEvent;
}
protected getiCalEventAsString(): string | undefined {
// Taking care of recurrence rule beforehand
let recurrenceRule: string | undefined = undefined;
if (this.recurringEvent?.count) {
recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", "");
}
const icsEvent = createEvent({
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
title: this.calEvent.attendees[0].language.translate("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
}),
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
...{ recurrenceRule },
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
protected getNodeMailerPayload(): Record<string, unknown> {
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
to: `${this.attendee.name} <${this.attendee.email}>`,
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
replyTo: this.calEvent.organizer.email,
subject: `${this.calEvent.attendees[0].language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getDescription(): string {
if (!this.calEvent.description) return "";
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("description")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px; white-space: pre-wrap;">${
this.calEvent.description
}</p>
</div>
`;
}
protected getTextBody(): string {
return `
${this.calEvent.attendees[0].language.translate(
this.recurringEvent?.count ? "your_event_has_been_scheduled_recurring" : "your_event_has_been_scheduled"
)}
${this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")}
${getRichDescription(this.calEvent)}
`.trim();
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.attendees[0].language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.team?.name || this.calEvent.organizer.name,
date: `${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("checkCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.attendees[0].language.translate(
this.recurringEvent?.count
? "your_event_has_been_scheduled_recurring"
: "your_event_has_been_scheduled"
),
this.calEvent.attendees[0].language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
${this.getManageLink()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
protected getManageLink(): string {
// Only the original attendee can make changes to the event
// Guests cannot
if (this.attendee === this.calEvent.attendees[0]) {
const manageText = this.calEvent.attendees[0].language.translate("manage_this_event");
return `<p>${this.calEvent.attendees[0].language.translate(
"need_to_reschedule_or_cancel"
)}</p><p style="font-weight: 400; line-height: 24px;"><a href="${getCancelLink(
this.calEvent
)}" style="color: #3E3E3E;" alt="${manageText}">${manageText}</a></p>`;
}
return "";
}
protected getWhat(): string {
return `
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("what")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.type}</p>
</div>`;
}
protected getRecurringWhen(): string {
if (this.recurringEvent?.freq) {
return ` - ${this.calEvent.attendees[0].language.translate("every_for_freq", {
freq: this.calEvent.attendees[0].language.translate(
`${rrule.FREQUENCIES[this.recurringEvent.freq].toString().toLowerCase()}`
),
})} ${this.recurringEvent.count} ${this.calEvent.attendees[0].language.translate(
`${rrule.FREQUENCIES[this.recurringEvent.freq].toString().toLowerCase()}`,
{ count: this.recurringEvent.count }
)}`;
} else {
return "";
}
}
protected getWhen(): string {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("when")}${
this.recurringEvent?.count ? this.getRecurringWhen() : ""
}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">
${this.recurringEvent?.count ? `${this.calEvent.attendees[0].language.translate("starting")} ` : ""}
${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("dddd").toLowerCase()
)}, ${this.calEvent.attendees[0].language.translate(
this.getInviteeStart().format("MMMM").toLowerCase()
)} ${this.getInviteeStart().format("D")}, ${this.getInviteeStart().format(
"YYYY"
)} | ${this.getInviteeStart().format("h:mma")} - ${this.getInviteeEnd().format(
"h:mma"
)} <span style="color: #888888">(${this.getTimezone()})</span>
</p>
</div>`;
}
protected getWho(): string {
const attendees = this.calEvent.attendees
.map((attendee) => {
return `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
attendee?.name || `${this.calEvent.attendees[0].language.translate("guest")}`
} <span style="color: #888888"><a href="mailto:${attendee.email}" style="color: #888888;">${
attendee.email
}</a></span></div>`;
})
.join("");
const organizer = `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
this.calEvent.organizer.name
} - ${this.calEvent.attendees[0].language.translate(
"organizer"
)} <span style="color: #888888"><a href="mailto:${
this.calEvent.organizer.email
}" style="color: #888888;">${this.calEvent.organizer.email}</a></span></div>`;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("who")}</p>
${organizer + attendees}
</div>`;
}
protected getAdditionalNotes(): string {
if (!this.calEvent.additionalNotes) return "";
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("additional_notes")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px; white-space: pre-wrap;">${
this.calEvent.additionalNotes
}</p>
</div>
`;
}
protected getCustomInputs(): string {
const { customInputs } = this.calEvent;
if (!customInputs) return "";
const customInputsString = Object.keys(customInputs)
.map((key) => {
if (customInputs[key] !== "") {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${key}</p>
<p style="color: #494949; font-weight: 400;">
${customInputs[key]}
</p>
</div>
`;
}
})
.join("");
return customInputsString;
}
protected getRejectionReason(): string {
if (!this.calEvent.rejectionReason) return "";
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("rejection_reason")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.rejectionReason}</p>
</div>`;
}
protected getLocation(): string {
let providerName = this.calEvent.location && getAppName(this.calEvent.location);
if (this.calEvent.location && this.calEvent.location.includes("integrations:")) {
const location = this.calEvent.location.split(":")[1];
providerName = location[0].toUpperCase() + location.slice(1);
}
// If location its a url, probably we should be validating it with a custom library
if (this.calEvent.location && /^https?:\/\//.test(this.calEvent.location)) {
providerName = this.calEvent.location;
}
if (this.calEvent.videoCallData) {
const meetingId = this.calEvent.videoCallData.id;
const meetingPassword = this.calEvent.videoCallData.password;
const meetingUrl = this.calEvent.videoCallData.url;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
meetingUrl &&
`<a href="${meetingUrl}" target="_blank" alt="${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"></img></a>`
}</p>
${
meetingId &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.attendees[0].language.translate(
"meeting_id"
)}: <span>${meetingId}</span></div>`
}
${
meetingPassword &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.attendees[0].language.translate(
"meeting_password"
)}: <span>${meetingPassword}</span></div>`
}
${
meetingUrl &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}: <a href="${meetingUrl}" alt="${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${meetingUrl}</a></div>`
}
</div>
`;
}
if (this.calEvent.additionInformation?.hangoutLink) {
const hangoutLink: string = this.calEvent.additionInformation.hangoutLink;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
hangoutLink &&
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"></img></a>`
}</p>
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.attendees[0].language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
</div>
`;
}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.attendees[0].language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
providerName || this.calEvent.location
}</p>
${
providerName === "Zoom" || providerName === "Google"
? `<p style="color: #494949; font-weight: 400; line-height: 24px;">
${this.calEvent.organizer.language.translate("meeting_url_provided_after_confirmed")}
</p>`
: ``
}
</div>
`;
}
protected getTimezone(): string {
// Timezone is based on the first attendee in the attendee list
// as the first attendee is the one who created the booking
return this.calEvent.attendees[0].timeZone;
}
protected getInviteeStart(): Dayjs {
return dayjs(this.calEvent.startTime).tz(this.getTimezone());
}
protected getInviteeEnd(): Dayjs {
return dayjs(this.calEvent.endTime).tz(this.getTimezone());
}
}
@@ -1,71 +0,0 @@
import { IS_PRODUCTION, BASE_URL } from "@lib/config/constants";
export type BodyHeadType = "checkCircle" | "xCircle" | "calendarCircle";
export const getHeadImage = (headerType: BodyHeadType): string => {
switch (headerType) {
case "checkCircle":
return IS_PRODUCTION
? BASE_URL + "/emails/checkCircle@2x.png"
: "https://app.cal.com/emails/checkCircle@2x.png";
case "xCircle":
return IS_PRODUCTION
? BASE_URL + "/emails/xCircle@2x.png"
: "https://app.cal.com/emails/xCircle@2x.png";
case "calendarCircle":
return IS_PRODUCTION
? BASE_URL + "/emails/calendarCircle@2x.png"
: "https://app.cal.com/emails/calendarCircle@2x.png";
}
};
export const emailSchedulingBodyHeader = (headerType: BodyHeadType): string => {
const image = getHeadImage(headerType);
return `
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;padding-top:40px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;border-top:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:30px 30px 0 30px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:558px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:64px;">
<img height="64" src="${image}" style="border:0;display:block;outline:none;text-decoration:none;height:64px;width:100%;font-size:13px;" width="64" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
`;
};
@@ -1,118 +0,0 @@
import BaseEmail from "@lib/emails/templates/_base-email";
import { emailHead, emailBodyLogo } from "./common";
export interface Feedback {
userId: number;
rating: string;
comment: string;
}
export default class FeedbackEmail extends BaseEmail {
feedback: Feedback;
constructor(feedback: Feedback) {
super();
this.feedback = feedback;
}
protected getNodeMailerPayload(): Record<string, unknown> {
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: process.env.SEND_FEEDBACK_EMAIL,
subject: `User Feedback`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
userId: ${this.feedback.userId}
rating: ${this.feedback.rating}
comment: ${this.feedback.comment}
`;
}
protected getHtmlBody(): string {
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead("Feedback")}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">Feedback</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<div style="background-color:#F5F5F5;">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
<div style="line-height: 6px;">
<p style="color: #494949;">Used id</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
this.feedback.userId
}</p>
</div>
<div style="line-height: 6px;">
<p style="color: #494949;">Rating</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
this.feedback.rating
}</p>
</div>
<div style="line-height: 6px;">
<p style="color: #494949;">Comment</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
this.feedback.comment
}</p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,189 +0,0 @@
import { TFunction } from "next-i18next";
import BaseEmail from "@lib/emails/templates/_base-email";
import { emailBodyLogo, emailHead, linkIcon } from "./common";
export type PasswordReset = {
language: TFunction;
user: {
name?: string | null;
email: string;
};
resetLink: string;
};
export const PASSWORD_RESET_EXPIRY_HOURS = 6;
export default class ForgotPasswordEmail extends BaseEmail {
passwordEvent: PasswordReset;
constructor(passwordEvent: PasswordReset) {
super();
this.name = "SEND_PASSWORD_RESET_EMAIL";
this.passwordEvent = passwordEvent;
}
protected getNodeMailerPayload(): Record<string, unknown> {
return {
to: `${this.passwordEvent.user.name} <${this.passwordEvent.user.email}>`,
from: `Cal.com <${this.getMailerOptions().from}>`,
subject: this.passwordEvent.language("reset_password_subject"),
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.passwordEvent.language("reset_password_subject")}
${this.passwordEvent.language("hi_user_name", { user: this.passwordEvent.user.name })},
${this.passwordEvent.language("someone_requested_password_reset")}
${this.passwordEvent.language("change_password")}: ${this.passwordEvent.resetLink}
${this.passwordEvent.language("password_reset_instructions")}
${this.passwordEvent.language("have_any_questions")} ${this.passwordEvent.language(
"contact_our_support_team"
)}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.passwordEvent.language("reset_password_subject");
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;padding-top:40px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;border-top:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:30px 20px 0 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
<div style="line-height: 6px;">
<p>${this.passwordEvent.language("hi_user_name", {
user: this.passwordEvent.user.name,
})},</p>
<p style="font-weight: 400; line-height: 24px;">${this.passwordEvent.language(
"someone_requested_password_reset"
)}</p>
</div>
</div>
</td>
</tr>
<tr>
<td align="left" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
<p style="display:inline-block;background:#292929;color:#292929;font-family:Roboto, Helvetica, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
<a href="${
this.passwordEvent.resetLink
}" target="_blank" style="color: #FFFFFF; text-decoration: none">${this.passwordEvent.language(
"change_password"
)} <img src="${linkIcon()}" width="12px"></img></a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
<div style="line-height: 6px;">
<p style="font-weight: 400; line-height: 24px;">${this.passwordEvent.language(
"password_reset_instructions"
)}</p>
</div>
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="font-weight: 400; line-height: 24px;">${this.passwordEvent.language(
"have_any_questions"
)} <a href="mailto:support@cal.com" style="color: #3E3E3E" target="_blank">${this.passwordEvent.language(
"contact_our_support_team"
)}</a></p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,152 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("event_cancelled_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("event_request_cancelled")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.cancellationReason && this.getCancellationReason()}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("event_cancelled_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("xCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("event_request_cancelled"),
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.cancellationReason && this.getCancellationReason()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
protected getCancellationReason(): string {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("cancellation_reason")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.cancellationReason}</p>
</div>`;
}
}
@@ -1,162 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("location_changed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("event_location_changed")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("location_changed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("event_location_changed"),
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
${this.getManageLink()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,207 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { emailHead, emailSchedulingBodyHeader, emailBodyLogo, emailSchedulingBodyDivider } from "./common";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerPaymentRefundFailedEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("refund_failed_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("a_refund_failed")}
${this.calEvent.organizer.language.translate("check_with_provider_and_user", {
user: this.calEvent.attendees[0].name,
})}
${
this.calEvent.paymentInfo &&
this.calEvent.organizer.language.translate("error_message", {
errorMessage: this.calEvent.paymentInfo.reason,
})
}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("refund_failed_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("xCircle")}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:24px;padding-bottom:0px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:24px;font-weight:700;line-height:24px;text-align:center;color:#292929;">${this.calEvent.organizer.language.translate(
"a_refund_failed"
)}</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.organizer.language.translate(
"check_with_provider_and_user",
{ user: this.calEvent.attendees[0].name }
)}</div>
</td>
</tr>
${this.getRefundInformation()}
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
protected getRefundInformation(): string {
const paymentInfo = this.calEvent.paymentInfo;
let refundInformation = "";
if (paymentInfo) {
if (paymentInfo.reason) {
refundInformation = `
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">${this.calEvent.organizer.language.translate(
"error_message",
{ errorMessage: paymentInfo.reason }
)}</div>
</td>
</tr>
`;
}
if (paymentInfo.id) {
refundInformation += `
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:400;line-height:24px;text-align:center;color:#494949;">Payment ${paymentInfo.id}</div>
</td>
</tr>
`;
}
}
return refundInformation;
}
}
@@ -1,178 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
linkIcon,
} from "./common";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("event_awaiting_approval_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("event_awaiting_approval")}
${this.calEvent.organizer.language.translate("someone_requested_an_event")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("confirm_or_reject_request")}
${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming"
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("event_awaiting_approval_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate(
this.recurringEvent?.count ? "event_awaiting_approval_recurring" : "event_awaiting_approval"
),
this.calEvent.organizer.language.translate("someone_requested_an_event")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
<p style="display:inline-block;background:#292929;color:#ffffff;font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
${this.getManageLink()}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;"></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
protected getManageLink(): string {
const manageText = this.calEvent.organizer.language.translate("confirm_or_reject_request");
const manageLink = process.env.NEXT_PUBLIC_WEBAPP_URL + "/bookings/upcoming";
return `<a style="color: #FFFFFF; text-decoration: none;" href="${manageLink}" target="_blank">${manageText} <img src="${linkIcon()}" width="12px"></img></a>`;
}
}
@@ -1,176 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
linkIcon,
} from "./common";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerRequestReminderEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("event_awaiting_approval_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("event_still_awaiting_approval")}
${this.calEvent.organizer.language.translate("someone_requested_an_event")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("confirm_or_reject_request")}
${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming"
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("event_awaiting_approval_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("event_still_awaiting_approval"),
this.calEvent.organizer.language.translate("someone_requested_an_event")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
<p style="display:inline-block;background:#292929;color:#ffffff;font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
${this.getManageLink()}
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:13px;line-height:1;text-align:left;color:#000000;"></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
protected getManageLink(): string {
const manageText = this.calEvent.organizer.language.translate("confirm_or_reject_request");
const manageLink = process.env.NEXT_PUBLIC_WEBAPP_URL + "/bookings/upcoming";
return `<a style="color: #FFFFFF; text-decoration: none;" href="${manageLink}" target="_blank">${manageText} <img src="${linkIcon()}" width="12px"></img></a>`;
}
}
@@ -1,190 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerRequestRescheduledEmail extends OrganizerScheduledEmail {
private metadata: { rescheduleLink: string };
constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }, recurringEvent: RecurringEvent) {
super(calEvent, recurringEvent);
this.metadata = metadata;
}
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
// @OVERRIDE
protected getiCalEventAsString(): string | undefined {
const icsEvent = createEvent({
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
title: this.calEvent.organizer.language.translate("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
}),
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
attendees: this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
status: "CANCELLED",
method: "CANCEL",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
// @OVERRIDE
protected getWhen(): string {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("when")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;text-decoration: line-through;">
${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format(
"YYYY"
)} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)} <span style="color: #888888">(${this.getTimezone()})</span>
</p>
</div>`;
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("request_reschedule_title_organizer", {
attendee: this.calEvent.attendees[0].name,
})}
${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
attendee: this.calEvent.attendees[0].name,
})},
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("request_reschedule_title_organizer", {
attendee: this.calEvent.attendees[0].name,
}),
this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", {
attendee: this.calEvent.attendees[0].name,
})
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,164 +0,0 @@
import dayjs from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import {
emailHead,
emailSchedulingBodyHeader,
emailBodyLogo,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
} from "./common";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate("event_has_been_rescheduled")}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${this.getWhat()}
${this.getWhen()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${getCancelLink(this.calEvent)}
`.replace(/(<([^>]+)>)/gi, "");
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("calendarCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate("event_has_been_rescheduled"),
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
${this.getManageLink()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
@@ -1,442 +0,0 @@
import dayjs, { Dayjs } from "dayjs";
import localizedFormat from "dayjs/plugin/localizedFormat";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import rrule from "rrule";
import { getAppName } from "@calcom/app-store/utils";
import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser";
import type { CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import BaseEmail from "@lib/emails/templates/_base-email";
import {
emailBodyLogo,
emailHead,
emailScheduledBodyHeaderContent,
emailSchedulingBodyDivider,
emailSchedulingBodyHeader,
linkIcon,
} from "./common";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(localizedFormat);
dayjs.extend(toArray);
export default class OrganizerScheduledEmail extends BaseEmail {
calEvent: CalendarEvent;
recurringEvent: RecurringEvent;
constructor(calEvent: CalendarEvent, recurringEvent: RecurringEvent) {
super();
this.name = "SEND_BOOKING_CONFIRMATION";
this.calEvent = calEvent;
this.recurringEvent = recurringEvent;
}
protected getiCalEventAsString(): string | undefined {
// Taking care of recurrence rule beforehand
let recurrenceRule: string | undefined = undefined;
if (this.recurringEvent?.count) {
recurrenceRule = new rrule(this.recurringEvent).toString().replace("RRULE:", "");
}
const icsEvent = createEvent({
start: dayjs(this.calEvent.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
startInputType: "utc",
productId: "calendso/ics",
title: this.calEvent.organizer.language.translate("ics_event_title", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
}),
description: this.getTextBody(),
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
...{ recurrenceRule },
attendees: this.calEvent.attendees.map((attendee: Person) => ({
name: attendee.name,
email: attendee.email,
})),
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
protected getNodeMailerPayload(): Record<string, unknown> {
const toAddresses = [this.calEvent.organizer.email];
if (this.calEvent.team) {
this.calEvent.team.members.forEach((member) => {
const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member);
if (memberAttendee) {
toAddresses.push(memberAttendee.email);
}
});
}
return {
icalEvent: {
filename: "event.ics",
content: this.getiCalEventAsString(),
},
from: `Cal.com <${this.getMailerOptions().from}>`,
to: toAddresses.join(","),
subject: `${this.calEvent.organizer.language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
})}`,
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `
${this.calEvent.organizer.language.translate(
this.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled"
)}
${this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")}
${getRichDescription(this.calEvent)}
`.trim();
}
protected getHtmlBody(): string {
const headerContent = this.calEvent.organizer.language.translate("confirmed_event_type_subject", {
eventType: this.calEvent.type,
name: this.calEvent.attendees[0].name,
date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailSchedulingBodyHeader("checkCircle")}
${emailScheduledBodyHeaderContent(
this.calEvent.organizer.language.translate(
this.recurringEvent?.count ? "new_event_scheduled_recurring" : "new_event_scheduled"
),
this.calEvent.organizer.language.translate("emailed_you_and_any_other_attendees")
)}
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
${this.getWhat()}
${this.getWhen()}
${this.getWho()}
${this.getLocation()}
${this.getDescription()}
${this.getAdditionalNotes()}
${this.getCustomInputs()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailSchedulingBodyDivider()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
${this.getManageLink()}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
protected getManageLink(): string {
const manageText = this.calEvent.organizer.language.translate("manage_this_event");
return `<p>${this.calEvent.organizer.language.translate(
"need_to_reschedule_or_cancel"
)}</p><p style="font-weight: 400; line-height: 24px;"><a href="${getCancelLink(
this.calEvent
)}" style="color: #3E3E3E;" alt="${manageText}">${manageText}</a></p>`;
}
protected getWhat(): string {
return `
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("what")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.type}</p>
</div>`;
}
protected getRecurringWhen(): string {
if (this.recurringEvent?.freq) {
return ` - ${this.calEvent.attendees[0].language.translate("every_for_freq", {
freq: this.calEvent.attendees[0].language.translate(
`${rrule.FREQUENCIES[this.recurringEvent.freq].toString().toLowerCase()}`
),
})} ${this.recurringEvent.count} ${this.calEvent.attendees[0].language.translate(
`${rrule.FREQUENCIES[this.recurringEvent.freq].toString().toLowerCase()}`,
{ count: this.recurringEvent.count }
)}`;
} else {
return "";
}
}
protected getWhen(): string {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("when")}${
this.recurringEvent?.count ? this.getRecurringWhen() : ""
}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">
${this.recurringEvent?.count ? `${this.calEvent.attendees[0].language.translate("starting")} ` : ""}
${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("dddd").toLowerCase()
)}, ${this.calEvent.organizer.language.translate(
this.getOrganizerStart().format("MMMM").toLowerCase()
)} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format(
"YYYY"
)} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format(
"h:mma"
)} <span style="color: #888888">(${this.getTimezone()})</span>
</p>
</div>`;
}
protected getWho(): string {
const attendees = this.calEvent.attendees
.map((attendee) => {
return `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
attendee?.name || `${this.calEvent.organizer.language.translate("guest")}`
} <span style="color: #888888"><a href="mailto:${attendee.email}" style="color: #888888;">${
attendee.email
}</a></span></div>`;
})
.join("");
const organizer = `<div style="color: #494949; font-weight: 400; line-height: 24px;">${
this.calEvent.organizer.name
} - ${this.calEvent.organizer.language.translate(
"organizer"
)} <span style="color: #888888"><a href="mailto:${
this.calEvent.organizer.email
}" style="color: #888888;">${this.calEvent.organizer.email}</a></span></div>`;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("who")}</p>
${organizer + attendees}
</div>`;
}
protected getAdditionalNotes(): string {
if (!this.calEvent.additionalNotes) return "";
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("additional_notes")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px; white-space: pre-wrap;">${
this.calEvent.additionalNotes
}</p>
</div>
`;
}
protected getCustomInputs(): string {
const { customInputs } = this.calEvent;
if (!customInputs) return "";
const customInputsString = Object.keys(customInputs)
.map((key) => {
if (customInputs[key] !== "") {
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${key}</p>
<p style="color: #494949; font-weight: 400;">
${customInputs[key]}
</p>
</div>
`;
}
})
.join("");
return customInputsString;
}
protected getDescription(): string {
if (!this.calEvent.description) return "";
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("description")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px; white-space: pre-wrap;">${
this.calEvent.description
}</p>
</div>
`;
}
protected getLocation(): string {
let providerName = this.calEvent.location && getAppName(this.calEvent.location); // This returns null if nothing is found
if (this.calEvent.location && this.calEvent.location.includes("integrations:")) {
const location = this.calEvent.location.split(":")[1];
providerName = location[0].toUpperCase() + location.slice(1);
}
// If location its a url, probably we should be validating it with a custom library
if (this.calEvent.location && /^https?:\/\//.test(this.calEvent.location)) {
providerName = this.calEvent.location;
}
if (this.calEvent.videoCallData) {
const meetingId = this.calEvent.videoCallData.id;
const meetingPassword = this.calEvent.videoCallData.password;
const meetingUrl = this.calEvent.videoCallData.url;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName}
${
meetingUrl &&
`<a href="${meetingUrl}" target="_blank" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"/></a>`
}</p>
${
meetingId &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.organizer.language.translate(
"meeting_id"
)}: <span>${meetingId}</span></div>`
}
${
meetingPassword &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.organizer.language.translate(
"meeting_password"
)}: <span>${meetingPassword}</span></div>`
}
${
meetingUrl &&
`<div style="color: #494949; font-weight: 400; line-height: 24px;">${this.calEvent.organizer.language.translate(
"meeting_url"
)}: <a href="${meetingUrl}" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${meetingUrl}</a></div>`
}
</div>
`;
}
if (this.calEvent.additionInformation?.hangoutLink) {
const hangoutLink: string = this.calEvent.additionInformation.hangoutLink;
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${providerName} ${
hangoutLink &&
`<a href="${hangoutLink}" target="_blank" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}"><img src="${linkIcon()}" width="12px"></img></a>`
}</p>
<div style="color: #494949; font-weight: 400; line-height: 24px;"><a href="${hangoutLink}" alt="${this.calEvent.organizer.language.translate(
"meeting_url"
)}" style="color: #3E3E3E" target="_blank">${hangoutLink}</a></div>
</div>
`;
}
return `
<p style="height: 6px"></p>
<div style="line-height: 6px;">
<p style="color: #494949;">${this.calEvent.organizer.language.translate("where")}</p>
<p style="color: #494949; font-weight: 400; line-height: 24px;">${
providerName || this.calEvent.location
}</p>
${
providerName === "Zoom" || providerName === "Google"
? `<p style="color: #494949; font-weight: 400; line-height: 24px;">
${this.calEvent.organizer.language.translate("meeting_url_provided_after_confirmed")}
</p>`
: ``
}
</div>
`;
}
protected getTimezone(): string {
return this.calEvent.organizer.timeZone;
}
protected getOrganizerStart(): Dayjs {
return dayjs(this.calEvent.startTime).tz(this.getTimezone());
}
protected getOrganizerEnd(): Dayjs {
return dayjs(this.calEvent.endTime).tz(this.getTimezone());
}
}
@@ -1,179 +0,0 @@
import { TFunction } from "next-i18next";
import BaseEmail from "@lib/emails/templates/_base-email";
import { emailBodyLogo, emailHead, linkIcon } from "./common";
export type TeamInvite = {
language: TFunction;
from: string;
to: string;
teamName: string;
joinLink: string;
};
export default class TeamInviteEmail extends BaseEmail {
teamInviteEvent: TeamInvite;
constructor(teamInviteEvent: TeamInvite) {
super();
this.name = "SEND_TEAM_INVITE_EMAIL";
this.teamInviteEvent = teamInviteEvent;
}
protected getNodeMailerPayload(): Record<string, unknown> {
return {
to: this.teamInviteEvent.to,
from: `Cal.com <${this.getMailerOptions().from}>`,
subject: this.teamInviteEvent.language("user_invited_you", {
user: this.teamInviteEvent.from,
team: this.teamInviteEvent.teamName,
}),
html: this.getHtmlBody(),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return "";
}
protected getHtmlBody(): string {
const headerContent = this.teamInviteEvent.language("user_invited_you", {
user: this.teamInviteEvent.from,
team: this.teamInviteEvent.teamName,
});
return `
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
${emailHead(headerContent)}
<body style="word-spacing:normal;background-color:#F5F5F5;">
<div style="background-color:#F5F5F5;">
${emailBodyLogo()}
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;padding-top:40px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;border-top:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:30px 20px 0 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
<div style="line-height: 6px;">
<p>${this.teamInviteEvent.language("user_invited_you", {
user: this.teamInviteEvent.from,
team: this.teamInviteEvent.teamName,
})}!</p>
<p style="font-weight: 400; line-height: 24px;">${this.teamInviteEvent.language(
"calcom_explained"
)}</p>
</div>
</div>
</td>
</tr>
<tr>
<td align="left" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#292929" role="presentation" style="border:none;border-radius:3px;cursor:auto;mso-padding-alt:10px 25px;background:#292929;" valign="middle">
<p style="display:inline-block;background:#292929;color:#292929;font-family:Roboto, Helvetica, sans-serif;font-size:13px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:3px;">
<a href="${
this.teamInviteEvent.joinLink
}" target="_blank" style="color: #FFFFFF; text-decoration: none">${this.teamInviteEvent.language(
"accept_invitation"
)} <img src="${linkIcon()}" width="12px"></img></a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
<div style="line-height: 6px;">
<p style="font-weight: 400; line-height: 24px;">${this.teamInviteEvent.language(
"have_any_questions"
)} <a href="mailto:support@cal.com" style="color: #3E3E3E" target="_blank">${this.teamInviteEvent.language(
"contact_our_support_team"
)}</a></p>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>
`;
}
}
+51 -8
View File
@@ -5,15 +5,58 @@ type EventNameObjectType = {
eventType: string;
eventName?: string | null;
host: string;
location?: string;
t: TFunction;
};
export function getEventName(eventNameObj: EventNameObjectType) {
return eventNameObj.eventName
? eventNameObj.eventName.replace("{USER}", eventNameObj.attendeeName)
: eventNameObj.t("event_between_users", {
eventName: eventNameObj.eventType,
host: eventNameObj.host,
attendeeName: eventNameObj.attendeeName,
});
export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView = false) {
if (!eventNameObj.eventName)
return eventNameObj.t("event_between_users", {
eventName: eventNameObj.eventType,
host: eventNameObj.host,
attendeeName: eventNameObj.attendeeName,
});
let eventName = eventNameObj.eventName;
let locationString = "";
if (eventNameObj.eventName.includes("{LOCATION}")) {
switch (eventNameObj.location) {
case "inPerson":
locationString = "In Person";
break;
case "userPhone":
case "phone":
locationString = "Phone";
break;
case "integrations:daily":
locationString = "Cal Video";
break;
case "integrations:zoom":
locationString = "Zoom";
break;
case "integrations:huddle01":
locationString = "Huddle01";
break;
case "integrations:tandem":
locationString = "Tandem";
break;
case "integrations:office365_video":
locationString = "MS Teams";
break;
case "integrations:jitsi":
locationString = "Jitsi";
break;
}
eventName = eventName.replace("{LOCATION}", locationString);
}
return (
eventName
// Need this for compatibility with older event names
.replace("{USER}", eventNameObj.attendeeName)
.replace("{ATTENDEE}", eventNameObj.attendeeName)
.replace("{HOST}", eventNameObj.host)
.replace("{HOST/ATTENDEE}", forAttendeeView ? eventNameObj.host : eventNameObj.attendeeName)
);
}
+8 -19
View File
@@ -1,4 +1,4 @@
import { Credential, SelectedCalendar } from "@prisma/client";
import { BookingStatus, Credential, SelectedCalendar } from "@prisma/client";
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
import { getBusyVideoTimes } from "@calcom/core/videoClient";
@@ -19,24 +19,13 @@ async function getBusyTimes(params: {
const busyTimes: EventBusyDate[] = await prisma.booking
.findMany({
where: {
AND: [
{
userId,
eventTypeId,
startTime: { gte: new Date(startTime) },
endTime: { lte: new Date(endTime) },
},
{
OR: [
{
status: "ACCEPTED",
},
{
status: "PENDING",
},
],
},
],
userId,
eventTypeId,
startTime: { gte: new Date(startTime) },
endTime: { lte: new Date(endTime) },
status: {
in: [BookingStatus.ACCEPTED],
},
},
select: {
startTime: true,
+1 -1
View File
@@ -3,7 +3,7 @@ import React from "react";
const isInteractionObserverSupported = typeof window !== "undefined" && "IntersectionObserver" in window;
export const useInViewObserver = (onInViewCallback: () => void) => {
const [node, setRef] = React.useState<any>(null);
const [node, setRef] = React.useState<HTMLElement | null>(null);
const onInViewCallbackRef = React.useRef(onInViewCallback);
onInViewCallbackRef.current = onInViewCallback;
+4 -14
View File
@@ -1,12 +1,10 @@
import { Prisma, UserPlan } from "@prisma/client";
import { baseEventTypeSelect } from "@calcom/prisma";
import prisma from "@lib/prisma";
type AsyncReturnType<T extends (...args: any) => Promise<any>> = T extends (...args: any) => Promise<infer R>
? R
: any;
export type TeamWithMembers = AsyncReturnType<typeof getTeamWithMembers>;
export type TeamWithMembers = Awaited<ReturnType<typeof getTeamWithMembers>>;
export async function getTeamWithMembers(id?: number, slug?: string) {
const userSelect = Prisma.validator<Prisma.UserSelect>()({
@@ -37,18 +35,10 @@ export async function getTeamWithMembers(id?: number, slug?: string) {
hidden: false,
},
select: {
id: true,
title: true,
description: true,
length: true,
slug: true,
schedulingType: true,
recurringEvent: true,
price: true,
currency: true,
users: {
select: userSelect,
},
...baseEventTypeSelect,
},
},
});
+80 -83
View File
@@ -1,17 +1,15 @@
import { jitsuClient, JitsuClient } from "@jitsu/sdk-js";
import React, { useContext } from "react";
import { NextApiRequest, NextApiResponse } from "next";
import { EventHandler, EventSinkOpts } from "next-collect";
import { useCollector } from "next-collect/client";
// it's ok to do this since we're importing only types which are harmless
// eslint-disable-next-line @next/next/no-server-import-in-page
import type { NextRequest, NextResponse } from "next/server";
declare global {
// eslint-disable-next-line no-var
var jitsu: JitsuClient | undefined;
}
import { CONSOLE_URL } from "@calcom/lib/constants";
/**
* Enumeration of all event types that are being sent
* to telemetry collection.
*/
export const telemetryEventTypes = {
pageView: "page_view",
apiCall: "api_call",
bookingConfirmed: "booking_confirmed",
bookingCancelled: "booking_cancelled",
importSubmitted: "import_submitted",
@@ -23,93 +21,92 @@ export const telemetryEventTypes = {
embedBookingConfirmed: "embed_booking_confirmed",
};
/**
* Telemetry client
*/
export type TelemetryClient = {
/**
* Use it as: withJitsu((jitsu) => {return jitsu.track()}). If telemetry is disabled, the callback will ignored
*
* ATTENTION: always return the value of jitsu.track() or id() call. Otherwise unhandled rejection can happen,
* which is handled in Next.js with a popup.
*/
withJitsu: (callback: (jitsu: JitsuClient) => void | Promise<void>) => void;
};
const emptyClient: TelemetryClient = {
withJitsu: () => {
// empty
},
};
function useTelemetry(): TelemetryClient {
return useContext(TelemetryContext);
}
function isLocalhost(host: string) {
return "localhost" === host || "127.0.0.1" === host;
}
/**
* Collects page parameters and makes sure no sensitive data made it to telemetry
* @param route current next.js route
*/
export function collectPageParameters(
route?: string,
extraData: Record<string, unknown> = {}
): Record<string, unknown> {
const host = document.location.hostname;
const maskedHost = isLocalhost(host) ? "localhost" : "masked";
//starts with ''
const host = document.location.host;
const docPath = route ?? "";
return {
page_url: route,
page_title: "",
source_ip: "",
doc_encoding: document.characterSet,
url: document.location.protocol + "//" + host + (docPath ?? ""),
doc_host: maskedHost,
doc_search: "",
doc_path: docPath,
referer: "",
...extraData,
};
}
function createTelemetryClient(): TelemetryClient {
if (process.env.NEXT_PUBLIC_TELEMETRY_KEY) {
return {
withJitsu: (callback) => {
if (!process.env.NEXT_PUBLIC_TELEMETRY_KEY) {
//telemetry is disabled
return;
}
if (!window) {
console.warn("Jitsu has been called during SSR, this scenario isn't supported yet");
return;
} else if (!window["jitsu"]) {
window["jitsu"] = jitsuClient({
log_level: "ERROR",
tracking_host: "https://t.calendso.com",
key: process.env.NEXT_PUBLIC_TELEMETRY_KEY,
cookie_name: "__clnds",
capture_3rd_party_cookies: false,
});
}
const res = callback(window["jitsu"]);
if (res && typeof res["catch"] === "function") {
res.catch((e) => {
console.debug("Unable to send telemetry event", e);
});
}
},
};
const reportUsage: EventHandler = async (event, { fetch }) => {
if (event.eventType === "booking") {
const key = process.env.CALCOM_LICENSE_KEY;
const url = `${CONSOLE_URL}/api/deployments/usage?key=${key}&quantity=1`;
try {
return fetch(url, { method: "POST", mode: "cors" });
} catch (e) {
console.error(`Error reporting booking for key: '${key}'`, e);
return Promise.resolve();
}
} else {
return emptyClient;
return Promise.resolve();
}
}
};
const TelemetryContext = React.createContext<TelemetryClient>(emptyClient);
export const nextCollectBasicSettings: EventSinkOpts = {
drivers: [
process.env.CALCOM_LICENSE_KEY && reportUsage,
process.env.CALCOM_TELEMETRY_DISABLED !== "1"
? {
type: "jitsu",
opts: {
key: "s2s.2pvs2bbpqq1zxna97wcml.esb6cikfrf7yn0qoh1nj1",
server: "https://t.calendso.com",
},
}
: undefined,
process.env.TELEMETRY_DEBUG && { type: "echo", opts: { disableColor: true } },
],
eventTypes: [
{ "*.ttf": null },
{ "*.webmanifest": null },
{ "*.json": null },
{ "*.svg": null },
{ "*.map": null },
{ "*.png": null },
{ "*.gif": null },
{ "/api/collect-events": null },
{ "/api*": null },
{ "/img*": null },
{ "/favicon*": null },
{ "/*": telemetryEventTypes.pageView },
],
};
const TelemetryProvider = TelemetryContext.Provider;
export const extendEventData = (
req: NextRequest | NextApiRequest,
res: NextResponse | NextApiResponse,
original: any
) => {
const onVercel =
typeof req.headers?.get === "function"
? !!req.headers.get("x-vercel-id")
: !!(req.headers as any)?.["x-vercel-id"];
const pageUrl = original?.page_url || (req as any)?.page?.name || undefined;
return {
title: "",
ipAddress: "",
queryString: "",
page_url: pageUrl,
licenseConsent: !!process.env.NEXT_PUBLIC_LICENSE_CONSENT,
licensekey: process.env.CALCOM_LICENSE_KEY,
isTeamBooking:
original?.isTeamBooking === undefined
? pageUrl?.includes("team/") || undefined
: original?.isTeamBooking,
referrer: "",
onVercel,
isAuthorized:
!!req.cookies["next-auth.session-token"] || !!req.cookies["__Secure-next-auth.session-token"],
utc_time: new Date().toISOString(),
};
};
export { TelemetryContext, TelemetryProvider, createTelemetryClient, useTelemetry };
export const useTelemetry = useCollector;
-5
View File
@@ -1,10 +1,5 @@
import { Attendee, Booking } from "@prisma/client";
export type BookingConfirmBody = {
confirmed: boolean;
id: number;
};
export type BookingCreateBody = {
email: string;
end: string;
@@ -0,0 +1,22 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
type Handlers = {
[method in "GET" | "POST" | "PATCH" | "PUT" | "DELETE"]?: Promise<{ default: NextApiHandler }>;
};
/** Allows us to split big API handlers by method and auto catch unsupported methods */
const defaultHandler = (handlers: Handlers) => async (req: NextApiRequest, res: NextApiResponse) => {
const handler = (await handlers[req.method as keyof typeof handlers])?.default;
if (!handler) return res.status(405).json({ message: "Method not allowed" });
try {
await handler(req, res);
return;
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Something went wrong" });
}
};
export default defaultHandler;
@@ -0,0 +1,38 @@
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
import { ZodError } from "zod";
import { HttpError } from "@calcom/lib/http-error";
type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>;
/** Allows us to get type inference from API handler responses */
function defaultResponder<T>(f: Handle<T>) {
return async (req: NextApiRequest, res: NextApiResponse) => {
try {
const result = await f(req, res);
res.json(result);
} catch (err) {
if (err instanceof HttpError) {
res.statusCode = err.statusCode;
res.json({ message: err.message });
} else if (err instanceof Stripe.errors.StripeInvalidRequestError) {
console.error("err", err);
res.statusCode = err.statusCode || 500;
res.json({ message: "Stripe error: " + err.message });
} else if (err instanceof ZodError && err.name === "ZodError") {
console.error("err", JSON.parse(err.message)[0].message);
res.statusCode = 400;
res.json({
message: "Validation errors: " + err.issues.map((i) => `—'${i.path}' ${i.message}`).join(". "),
});
} else {
console.error("err", err);
res.statusCode = 500;
res.json({ message: "Unknown error" });
}
}
};
}
export default defaultResponder;
+2
View File
@@ -0,0 +1,2 @@
export { default as defaultHandler } from "./defaultHandler";
export { default as defaultResponder } from "./defaultResponder";
+1
View File
@@ -0,0 +1 @@
export * from "./api";
+6
View File
@@ -8,11 +8,15 @@ const withTM = require("next-transpile-modules")([
"@calcom/prisma",
"@calcom/stripe",
"@calcom/ui",
"@calcom/emails",
"@calcom/embed-core",
"@calcom/embed-snippet",
]);
const { i18n } = require("./next-i18next.config");
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
// So we can test deploy previews preview
if (process.env.VERCEL_URL && !process.env.NEXT_PUBLIC_WEBAPP_URL) {
process.env.NEXT_PUBLIC_WEBAPP_URL = "https://" + process.env.VERCEL_URL;
@@ -32,6 +36,8 @@ if (!process.env.EMAIL_FROM) {
);
}
if (!process.env.NEXTAUTH_URL) throw new Error("Please set NEXTAUTH_URL");
const validJson = (jsonString) => {
try {
const o = JSON.parse(jsonString);
+1 -1
View File
@@ -41,7 +41,6 @@
"@heroicons/react": "^1.0.6",
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.8.9",
"@jitsu/sdk-js": "^2.2.4",
"@metamask/providers": "^8.1.1",
"@next-auth/prisma-adapter": "^1.0.3",
"@next/bundle-analyzer": "12.1.6",
@@ -81,6 +80,7 @@
"mime-types": "^2.1.35",
"next": "^12.1.6",
"next-auth": "^4.3.4",
"next-collect": "^0.1.0",
"next-i18next": "^11.0.0",
"next-mdx-remote": "^4.0.3",
"next-seo": "^4.26.0",
+8 -17
View File
@@ -23,6 +23,7 @@ import defaultEvents, {
getUsernameSlugLink,
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import useTheme from "@lib/hooks/useTheme";
@@ -122,13 +123,12 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
const telemetry = useTelemetry();
useEffect(() => {
telemetry.withJitsu((jitsu) =>
jitsu.track(
top !== window ? telemetryEventTypes.embedView : telemetryEventTypes.pageView,
collectPageParameters("/[user]")
)
);
}, [telemetry]);
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
}
}, [telemetry, router.asPath]);
return (
<>
<Theme />
@@ -275,17 +275,8 @@ const getEventTypesWithHiddenFromDB = async (userId: number, plan: UserPlan) =>
},
],
select: {
id: true,
slug: true,
title: true,
length: true,
description: true,
hidden: true,
schedulingType: true,
recurringEvent: true,
price: true,
currency: true,
metadata: true,
...baseEventTypeSelect,
},
take: plan === UserPlan.FREE ? 1 : undefined,
});
+3 -5
View File
@@ -21,7 +21,6 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage";
import { getTranslation } from "@server/lib/i18n";
import { ssrInit } from "@server/lib/ssr";
dayjs.extend(utc);
@@ -31,6 +30,8 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) {
const { t } = useLocale();
const locationLabels = getLocationLabels(t);
return props.away ? (
<div className="h-screen dark:bg-neutral-900">
<main className="mx-auto max-w-3xl px-4 py-24">
@@ -62,7 +63,7 @@ export default function Book(props: BookPageProps) {
</main>
</div>
) : (
<BookingPage {...props} />
<BookingPage {...props} locationLabels={locationLabels} />
);
}
@@ -215,8 +216,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
eventName: null,
};
const t = await getTranslation(context.locale ?? "en", "common");
// Checking if number of recurring event ocurrances is valid against event type configuration
const recurringEventCount =
(eventType.recurringEvent?.count &&
@@ -229,7 +228,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
away: user.away,
locationLabels: getLocationLabels(t),
profile,
eventType: eventTypeObject,
booking,
+20 -16
View File
@@ -1,3 +1,4 @@
import { EventCollectionProvider } from "next-collect/client";
import { DefaultSeo } from "next-seo";
import Head from "next/head";
import superjson from "superjson";
@@ -30,23 +31,26 @@ function MyApp(props: AppProps) {
pageStatus = "500";
}
return (
<ContractsProvider>
<AppProviders {...props}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<I18nLanguageHandler />
<Head>
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
{Component.requiresLicense ? (
<LicenseRequired>
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<ContractsProvider>
<AppProviders {...props}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
<I18nLanguageHandler />
<Head>
<script
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}></script>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
</Head>
{Component.requiresLicense ? (
<LicenseRequired>
<Component {...pageProps} err={err} />
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
</LicenseRequired>
) : (
<Component {...pageProps} err={err} />
)}
</AppProviders>
</ContractsProvider>
)}
</AppProviders>
</ContractsProvider>
</EventCollectionProvider>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { collectEvents } from "next-collect/server";
import { extendEventData, nextCollectBasicSettings } from "@lib/telemetry";
export default collectEvents({
...nextCollectBasicSettings,
cookieName: "__clnds",
extend: extendEventData,
});
+3 -3
View File
@@ -14,15 +14,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const userId = req.session.user.id;
try {
const installedApp = await prisma.credential.findFirst({
const installedApp = await prisma.credential.findMany({
where: {
type: appCredentialType as string,
userId: userId,
},
});
if (installedApp && !!installedApp.key) {
res.status(200);
if (installedApp && !!installedApp.length) {
res.json({ count: installedApp.length });
} else {
res.status(404);
}
+1 -1
View File
@@ -159,7 +159,7 @@ if (isSAMLLoginEnabled) {
}
if (true) {
const emailsDir = path.resolve(process.cwd(), "lib", "emails", "templates");
const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates");
providers.push(
EmailProvider({
maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only
+5 -6
View File
@@ -2,8 +2,9 @@ import { ResetPasswordRequest } from "@prisma/client";
import dayjs from "dayjs";
import { NextApiRequest, NextApiResponse } from "next";
import { sendPasswordResetEmail } from "@lib/emails/email-manager";
import { PasswordReset, PASSWORD_RESET_EXPIRY_HOURS } from "@lib/emails/templates/forgot-password-email";
import { sendPasswordResetEmail } from "@calcom/emails";
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
import prisma from "@lib/prisma";
import { getTranslation } from "@server/lib/i18n";
@@ -56,13 +57,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
const resetLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/forgot-password/${passwordRequest.id}`;
const passwordEmail: PasswordReset = {
await sendPasswordResetEmail({
language: t,
user: maybeUser,
resetLink,
};
await sendPasswordResetEmail(passwordEmail);
});
/** So we can test the password reset flow on CI */
if (process.env.NEXT_PUBLIC_IS_E2E) {
+215 -205
View File
@@ -1,20 +1,22 @@
import { Booking, BookingStatus, Prisma, SchedulingType, User } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextApiRequest } from "next";
import { z } from "zod";
import EventManager from "@calcom/core/EventManager";
import { sendDeclinedEmails, sendScheduledEmails } from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { AdditionInformation, CalendarEvent, RecurringEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { sendDeclinedEmails, sendScheduledEmails } from "@lib/emails/email-manager";
import prisma from "@lib/prisma";
import { BookingConfirmBody } from "@lib/types/booking";
import { HttpError } from "@lib/core/http/error";
import { getTranslation } from "@server/lib/i18n";
import { defaultHandler, defaultResponder } from "~/common";
const authorized = async (
currentUser: Pick<User, "id">,
booking: Pick<Booking, "eventTypeId" | "userId">
@@ -43,20 +45,30 @@ const authorized = async (
const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
const bookingConfirmPatchBodySchema = z.object({
confirmed: z.boolean(),
id: z.number(),
recurringEventId: z.string().optional(),
reason: z.string().optional(),
});
async function patchHandler(req: NextApiRequest) {
const session = await getSession({ req });
if (!session?.user?.id) {
return res.status(401).json({ message: "Not authenticated" });
throw new HttpError({ statusCode: 401, message: "Not authenticated" });
}
const reqBody = req.body as BookingConfirmBody;
const bookingId = reqBody.id;
if (!bookingId) {
return res.status(400).json({ message: "bookingId missing" });
}
const {
id: bookingId,
recurringEventId,
reason: rejectionReason,
confirmed,
} = bookingConfirmPatchBodySchema.parse(req.body);
const currentUser = await prisma.user.findFirst({
rejectOnNotFound() {
throw new HttpError({ statusCode: 404, message: "User not found" });
},
where: {
id: session.user.id,
},
@@ -74,230 +86,228 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
});
if (!currentUser) {
return res.status(404).json({ message: "User not found" });
}
const tOrganizer = await getTranslation(currentUser.locale ?? "en", "common");
if (req.method === "PATCH") {
const booking = await prisma.booking.findFirst({
const booking = await prisma.booking.findFirst({
where: {
id: bookingId,
},
rejectOnNotFound() {
throw new HttpError({ statusCode: 404, message: "Booking not found" });
},
select: {
title: true,
description: true,
customInputs: true,
startTime: true,
endTime: true,
attendees: true,
eventTypeId: true,
eventType: {
select: {
recurringEvent: true,
},
},
location: true,
userId: true,
id: true,
uid: true,
payment: true,
destinationCalendar: true,
paid: true,
recurringEventId: true,
status: true,
},
});
if (!(await authorized(currentUser, booking))) {
throw new HttpError({ statusCode: 401, message: "UNAUTHORIZED" });
}
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
if (isConfirmed) {
throw new HttpError({ statusCode: 400, message: "booking already confirmed" });
}
/** When a booking that requires payment its being confirmed but doesn't have any payment,
* we shouldn’t save it on DestinationCalendars
*/
if (booking.payment.length > 0 && !booking.paid) {
await prisma.booking.update({
where: {
id: bookingId,
},
select: {
title: true,
description: true,
customInputs: true,
startTime: true,
endTime: true,
confirmed: true,
attendees: true,
eventTypeId: true,
eventType: {
select: {
recurringEvent: true,
},
},
location: true,
userId: true,
id: true,
uid: true,
payment: true,
destinationCalendar: true,
paid: true,
recurringEventId: true,
data: {
status: BookingStatus.ACCEPTED,
},
});
if (!booking) {
return res.status(404).json({ message: "booking not found" });
}
req.statusCode = 204;
return { message: "Booking confirmed" };
}
if (!(await authorized(currentUser, booking))) {
return res.status(401).end();
}
if (booking.confirmed) {
return res.status(400).json({ message: "booking already confirmed" });
}
/** When a booking that requires payment its being confirmed but doesn't have any payment,
* we shouldn’t save it on DestinationCalendars
*/
if (booking.payment.length > 0 && !booking.paid) {
await prisma.booking.update({
where: {
id: bookingId,
},
data: {
confirmed: true,
},
});
return res.status(204).end();
}
const attendeesListPromises = booking.attendees.map(async (attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
};
});
const attendeesList = await Promise.all(attendeesListPromises);
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
description: booking.description,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {
email: currentUser.email,
name: currentUser.name || "Unnamed",
timeZone: currentUser.timeZone,
language: { translate: tOrganizer, locale: currentUser.locale ?? "en" },
const attendeesListPromises = booking.attendees.map(async (attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
},
attendees: attendeesList,
location: booking.location ?? "",
uid: booking.uid,
destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar,
};
});
const recurringEvent = booking.eventType?.recurringEvent as RecurringEvent;
const attendeesList = await Promise.all(attendeesListPromises);
if (req.body.recurringEventId && recurringEvent) {
const groupedRecurringBookings = await prisma.booking.groupBy({
where: {
recurringEventId: booking.recurringEventId,
},
by: [Prisma.BookingScalarFieldEnum.recurringEventId],
_count: true,
});
// Overriding the recurring event configuration count to be the actual number of events booked for
// the recurring event (equal or less than recurring event configuration count)
recurringEvent.count = groupedRecurringBookings[0]._count;
const evt: CalendarEvent = {
type: booking.title,
title: booking.title,
description: booking.description,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {
email: currentUser.email,
name: currentUser.name || "Unnamed",
timeZone: currentUser.timeZone,
language: { translate: tOrganizer, locale: currentUser.locale ?? "en" },
},
attendees: attendeesList,
location: booking.location ?? "",
uid: booking.uid,
destinationCalendar: booking?.destinationCalendar || currentUser.destinationCalendar,
};
const recurringEvent = booking.eventType?.recurringEvent as RecurringEvent;
if (recurringEventId && recurringEvent) {
const groupedRecurringBookings = await prisma.booking.groupBy({
where: {
recurringEventId: booking.recurringEventId,
},
by: [Prisma.BookingScalarFieldEnum.recurringEventId],
_count: true,
});
// Overriding the recurring event configuration count to be the actual number of events booked for
// the recurring event (equal or less than recurring event configuration count)
recurringEvent.count = groupedRecurringBookings[0]._count;
}
if (confirmed) {
const eventManager = new EventManager(currentUser);
const scheduleResult = await eventManager.create(evt);
const results = scheduleResult.results;
if (results.length > 0 && results.every((res) => !res.success)) {
const error = {
errorCode: "BookingCreatingMeetingFailed",
message: "Booking failed",
};
log.error(`Booking ${currentUser.username} failed`, error, results);
} else {
const metadata: AdditionInformation = {};
if (results.length) {
// TODO: Handle created event metadata more elegantly
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
metadata.conferenceData = results[0].createdEvent?.conferenceData;
metadata.entryPoints = results[0].createdEvent?.entryPoints;
}
try {
await sendScheduledEmails(
{ ...evt, additionInformation: metadata },
recurringEventId ? recurringEvent : {} // Send email with recurring event info only on recurring event context
);
} catch (error) {
log.error(error);
}
}
if (reqBody.confirmed) {
const eventManager = new EventManager(currentUser);
const scheduleResult = await eventManager.create(evt);
const results = scheduleResult.results;
if (results.length > 0 && results.every((res) => !res.success)) {
const error = {
errorCode: "BookingCreatingMeetingFailed",
message: "Booking failed",
};
log.error(`Booking ${currentUser.username} failed`, error, results);
} else {
const metadata: AdditionInformation = {};
if (results.length) {
// TODO: Handle created event metadata more elegantly
metadata.hangoutLink = results[0].createdEvent?.hangoutLink;
metadata.conferenceData = results[0].createdEvent?.conferenceData;
metadata.entryPoints = results[0].createdEvent?.entryPoints;
}
try {
await sendScheduledEmails(
{ ...evt, additionInformation: metadata },
req.body.recurringEventId ? recurringEvent : {} // Send email with recurring event info only on recurring event context
);
} catch (error) {
log.error(error);
}
}
if (req.body.recurringEventId) {
// The booking to confirm is a recurring event and comes from /booking/upcoming, proceeding to mark all related
// bookings as confirmed. Prisma updateMany does not support relations, so doing this in two steps for now.
const unconfirmedRecurringBookings = await prisma.booking.findMany({
where: {
recurringEventId: req.body.recurringEventId,
confirmed: false,
},
});
unconfirmedRecurringBookings.map(async (recurringBooking) => {
await prisma.booking.update({
where: {
id: recurringBooking.id,
},
data: {
confirmed: true,
references: {
create: scheduleResult.referencesToCreate,
},
},
});
});
} else {
// @NOTE: be careful with this as if any error occurs before this booking doesn't get confirmed
// Should perform update on booking (confirm) -> then trigger the rest handlers
if (recurringEventId) {
// The booking to confirm is a recurring event and comes from /booking/upcoming, proceeding to mark all related
// bookings as confirmed. Prisma updateMany does not support relations, so doing this in two steps for now.
const unconfirmedRecurringBookings = await prisma.booking.findMany({
where: {
recurringEventId,
status: BookingStatus.PENDING,
},
});
unconfirmedRecurringBookings.map(async (recurringBooking) => {
await prisma.booking.update({
where: {
id: bookingId,
id: recurringBooking.id,
},
data: {
confirmed: true,
status: BookingStatus.ACCEPTED,
references: {
create: scheduleResult.referencesToCreate,
},
},
});
}
res.status(204).end();
});
} else {
const rejectionReason = asStringOrNull(req.body.reason) || "";
evt.rejectionReason = rejectionReason;
if (req.body.recurringEventId) {
// The booking to reject is a recurring event and comes from /booking/upcoming, proceeding to mark all related
// bookings as rejected. Prisma updateMany does not support relations, so doing this in two steps for now.
const unconfirmedRecurringBookings = await prisma.booking.findMany({
where: {
recurringEventId: req.body.recurringEventId,
confirmed: false,
// @NOTE: be careful with this as if any error occurs before this booking doesn't get confirmed
// Should perform update on booking (confirm) -> then trigger the rest handlers
await prisma.booking.update({
where: {
id: bookingId,
},
data: {
status: BookingStatus.ACCEPTED,
references: {
create: scheduleResult.referencesToCreate,
},
});
unconfirmedRecurringBookings.map(async (recurringBooking) => {
await prisma.booking.update({
where: {
id: recurringBooking.id,
},
data: {
rejected: true,
status: BookingStatus.REJECTED,
rejectionReason: rejectionReason,
},
});
});
} else {
await refund(booking, evt); // No payment integration for recurring events for v1
},
});
}
} else {
evt.rejectionReason = rejectionReason;
if (recurringEventId) {
// The booking to reject is a recurring event and comes from /booking/upcoming, proceeding to mark all related
// bookings as rejected. Prisma updateMany does not support relations, so doing this in two steps for now.
const unconfirmedRecurringBookings = await prisma.booking.findMany({
where: {
recurringEventId,
status: BookingStatus.PENDING,
},
});
unconfirmedRecurringBookings.map(async (recurringBooking) => {
await prisma.booking.update({
where: {
id: bookingId,
id: recurringBooking.id,
},
data: {
rejected: true,
status: BookingStatus.REJECTED,
rejectionReason: rejectionReason,
rejectionReason,
},
});
}
await sendDeclinedEmails(evt, req.body.recurringEventId ? recurringEvent : {}); // Send email with recurring event info only on recurring event context
res.status(204).end();
});
} else {
await refund(booking, evt); // No payment integration for recurring events for v1
await prisma.booking.update({
where: {
id: bookingId,
},
data: {
status: BookingStatus.REJECTED,
rejectionReason,
},
});
}
await sendDeclinedEmails(evt, recurringEventId ? recurringEvent : {}); // Send email with recurring event info only on recurring event context
}
req.statusCode = 204;
return { message: "Booking " + confirmed ? "confirmed" : "rejected" };
}
export type BookConfirmPatchResponse = Awaited<ReturnType<typeof patchHandler>>;
export default defaultHandler({
// To prevent too much git diff until moved to another file
PATCH: Promise.resolve({ default: defaultResponder(patchHandler) }),
});
+36 -21
View File
@@ -11,6 +11,12 @@ import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import EventManager from "@calcom/core/EventManager";
import {
sendAttendeeRequestEmail,
sendOrganizerRequestEmail,
sendRescheduledEmails,
sendScheduledEmails,
} from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { getErrorFromUnknown } from "@calcom/lib/errors";
@@ -20,12 +26,6 @@ import type { AdditionInformation, CalendarEvent, RecurringEvent } from "@calcom
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import { handlePayment } from "@ee/lib/stripe/server";
import {
sendAttendeeRequestEmail,
sendOrganizerRequestEmail,
sendRescheduledEmails,
sendScheduledEmails,
} from "@lib/emails/email-manager";
import { ensureArray } from "@lib/ensureArray";
import { getEventName } from "@lib/event";
import getBusyTimes from "@lib/getBusyTimes";
@@ -190,6 +190,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
hideCalendarNotes: true,
seatsPerTimeSlot: true,
recurringEvent: true,
locations: true,
},
});
@@ -201,7 +202,11 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
type User = Prisma.UserGetPayload<typeof userSelect>;
type ExtendedBookingCreateBody = BookingCreateBody & { noEmail?: boolean; recurringCount?: number };
type ExtendedBookingCreateBody = BookingCreateBody & {
noEmail?: boolean;
recurringCount?: number;
rescheduleReason?: string;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { recurringCount, noEmail, ...reqBody } = req.body as ExtendedBookingCreateBody;
@@ -256,10 +261,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!eventTypeUser) return res.status(404).json({ message: "eventTypeUser.notFound" });
users.push(eventTypeUser);
}
const [organizerUser] = users;
const organizer = await prisma.user.findUnique({
where: {
id: users[0].id,
id: organizerUser.id,
},
select: {
locale: true,
@@ -353,14 +358,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const attendeesList = [...invitee, ...guests, ...teamMembers];
const seed = `${users[0].username}:${dayjs(req.body.start).utc().format()}:${new Date().getTime()}`;
const seed = `${organizerUser.username}:${dayjs(req.body.start).utc().format()}:${new Date().getTime()}`;
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
const location = !!eventType.locations ? (eventType.locations as Array<{ type: string }>)[0] : "";
const locationType = !!location && location.type ? location.type : "";
const eventNameObject = {
attendeeName: reqBody.name || "Nameless",
eventType: eventType.title,
eventName: eventType.eventName,
host: users[0].name || "Nameless",
host: organizerUser.name || "Nameless",
location: locationType,
t: tOrganizer,
};
@@ -383,15 +391,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
startTime: reqBody.start,
endTime: reqBody.end,
organizer: {
name: users[0].name || "Nameless",
email: users[0].email || "Email-less",
timeZone: users[0].timeZone,
name: organizerUser.name || "Nameless",
email: organizerUser.email || "Email-less",
timeZone: organizerUser.timeZone,
language: { translate: tOrganizer, locale: organizer?.locale ?? "en" },
},
attendees: attendeesList,
location: reqBody.location, // Will be processed by the EventManager later.
/** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */
destinationCalendar: eventType.destinationCalendar || users[0].destinationCalendar,
destinationCalendar: eventType.destinationCalendar || organizerUser.destinationCalendar,
hideCalendarNotes: eventType.hideCalendarNotes,
};
@@ -470,6 +478,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null;
const dynamicGroupSlugRef = !eventTypeId ? (reqBody.user as string).toLowerCase() : null;
const isConfirmedByDefault = (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid;
const newBookingData: Prisma.BookingCreateInput = {
uid,
title: evt.title,
@@ -477,7 +486,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
endTime: dayjs(evt.endTime).toDate(),
description: evt.additionalNotes,
customInputs: isPrismaObjOrUndefined(evt.customInputs),
confirmed: (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid,
status: isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING,
location: evt.location,
eventType: eventTypeRel,
attendees: {
@@ -499,7 +508,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
dynamicGroupSlugRef,
user: {
connect: {
id: users[0].id,
id: organizerUser.id,
},
},
destinationCalendar: evt.destinationCalendar
@@ -543,7 +552,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const stripePaymentCredential = await prisma.credential.findFirst({
where: {
type: "stripe_payment",
userId: users[0].id,
userId: organizerUser.id,
},
select: {
id: true,
@@ -677,7 +686,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (originalRescheduledBooking?.uid) {
// Use EventManager to conditionally use all needed integrations.
const updateManager = await eventManager.update(evt, originalRescheduledBooking.uid, booking?.id);
const updateManager = await eventManager.update(
evt,
originalRescheduledBooking.uid,
booking?.id,
reqBody.rescheduleReason
);
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt.description = eventType.description;
@@ -711,7 +725,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{
...evt,
additionInformation: metadata,
additionalNotes, // Resets back to the addtionalNote input and not the overriden value
additionalNotes, // Resets back to the additionalNote input and not the override value
cancellationReason: reqBody.rescheduleReason,
},
reqBody.recurringEventId ? (eventType.recurringEvent as RecurringEvent) : {}
);
@@ -821,7 +836,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await Promise.all(promises);
// Avoid passing referencesToCreate with id unique constrain values
// refresh hashed link if used
const urlSeed = `${users[0].username}:${dayjs(req.body.start).utc().format()}`;
const urlSeed = `${organizerUser.username}:${dayjs(req.body.start).utc().format()}`;
const hashedUid = translator.fromUUID(uuidv5(urlSeed, uuidv5.URL));
if (hasHashedBookingLink) {
@@ -9,10 +9,10 @@ import { getCalendar } from "@calcom/core/CalendarManager";
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
import { deleteMeeting } from "@calcom/core/videoClient";
import { sendRequestRescheduleEmail } from "@calcom/emails";
import { getTranslation } from "@calcom/lib/server/i18n";
import { Person } from "@calcom/types/Calendar";
import { sendRequestRescheduleEmail } from "@lib/emails/email-manager";
import prisma from "@lib/prisma";
export type RescheduleResponse = Booking & {
+2 -2
View File
@@ -6,6 +6,7 @@ import { NextApiRequest, NextApiResponse } from "next";
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getCalendar } from "@calcom/core/CalendarManager";
import { deleteMeeting } from "@calcom/core/videoClient";
import { sendCancelledEmails } from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
@@ -13,7 +14,6 @@ import { refund } from "@ee/lib/stripe/server";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { sendCancelledEmails } from "@lib/emails/email-manager";
import sendPayload from "@lib/webhooks/sendPayload";
import getWebhooks from "@lib/webhooks/subscriptions";
@@ -200,7 +200,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: bookingToDelete.id,
},
data: {
rejected: true,
status: BookingStatus.REJECTED,
},
});
+9
View File
@@ -0,0 +1,9 @@
import { nextEventsCollectApi } from "next-collect/server";
import { extendEventData, nextCollectBasicSettings } from "@lib/telemetry";
export default nextEventsCollectApi({
...nextCollectBasicSettings,
cookieName: "__clnds",
extend: extendEventData,
});
+3 -5
View File
@@ -1,13 +1,12 @@
import { ReminderType } from "@prisma/client";
import { BookingStatus, ReminderType } from "@prisma/client";
import dayjs from "dayjs";
import type { NextApiRequest, NextApiResponse } from "next";
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { sendOrganizerRequestReminderEmail } from "@lib/emails/email-manager";
import { getTranslation } from "@server/lib/i18n";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -26,8 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
for (const interval of reminderIntervalMinutes) {
const bookings = await prisma.booking.findMany({
where: {
confirmed: false,
rejected: false,
status: BookingStatus.PENDING,
createdAt: {
lte: dayjs().add(-interval, "minutes").toDate(),
},
+58
View File
@@ -0,0 +1,58 @@
import { NextApiRequest, NextApiResponse } from "next";
import { renderEmail } from "@calcom/emails";
import { getTranslation } from "@calcom/lib/server/i18n";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (process.env.NODE_ENV !== "development") return res.write("Only for development purposes"), res.end();
const t = await getTranslation("en", "common");
const evt = {
type: "30min",
title: "30min between Pro Example and pro@example.com",
description: null,
additionalNotes: "asdasdas",
customInputs: {
"Custom input 01": "sadasdasdsadasd",
"Custom input 02": "asdasdasd",
},
startTime: "2022-06-03T09:00:00-06:00",
endTime: "2022-06-03T09:30:00-06:00",
organizer: {
name: "Pro Example",
email: "pro@example.com",
timeZone: "Europe/London",
language: { translate: t, locale: "en" },
},
attendees: [
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language: { translate: t, locale: "en" },
},
],
location: "Zoom video",
destinationCalendar: null,
hideCalendarNotes: false,
uid: "bwPWLpjYrx4rZf6MCZdKgE",
metadata: {},
cancellationReason: "It got late",
paymentInfo: { id: "pi_12312", link: "https://cal.com", reason: "no reason" },
};
req.statusCode = 200;
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
renderEmail("OrganizerRequestReminderEmail", {
attendee: evt.attendees[0],
calEvent: evt,
recurringEvent: {},
})
);
res.end();
};
export default handler;
+9 -1
View File
@@ -5,6 +5,14 @@ import { getSession } from "@lib/auth";
const prisma = new PrismaClient();
type CalendlyEventType = {
name: string;
slug: string;
duration: number;
description_plain: string;
secret: boolean;
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const authenticatedUser = await prisma.user.findFirst({
@@ -50,7 +58,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const eventTypesData = await eventTypesResult.json();
eventTypesData.collection.forEach(async (eventType: any) => {
eventTypesData.collection.forEach(async (eventType: CalendlyEventType) => {
await prisma.eventType.create({
data: {
title: eventType.name,
+9 -1
View File
@@ -5,6 +5,14 @@ import { getSession } from "@lib/auth";
const prisma = new PrismaClient();
type SavvyCalEventType = {
name: string;
slug: string;
durations: [number];
description: string;
state: "active";
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });
const authenticatedUser = await prisma.user.findFirst({
@@ -50,7 +58,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const eventTypesData = await eventTypesResult.json();
eventTypesData.entries.forEach(async (eventType: any) => {
eventTypesData.entries.forEach(async (eventType: SavvyCalEventType) => {
await prisma.eventType.create({
data: {
title: eventType.name,
+4 -5
View File
@@ -1,11 +1,11 @@
import { Prisma } from "@prisma/client";
import { BookingStatus, Prisma } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
import prisma from "@lib/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!["GET", "DELETE"].includes(req.method!)) {
if (!["GET", "DELETE"].includes(req.method || "")) {
return res.status(405).end();
}
@@ -105,7 +105,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
},
data: {
status: "CANCELLED",
status: BookingStatus.CANCELLED,
rejectionReason: "Payment provider got removed",
},
});
@@ -113,8 +113,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const bookingReferences = await prisma.booking
.findMany({
where: {
confirmed: true,
rejected: false,
status: BookingStatus.ACCEPTED,
},
select: {
id: true,
+4 -3
View File
@@ -2,10 +2,11 @@ import { MembershipRole } from "@prisma/client";
import { randomBytes } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { sendTeamInviteEmail } from "@calcom/emails";
import { TeamInvite } from "@calcom/emails/templates/team-invite-email";
import { getSession } from "@lib/auth";
import { BASE_URL } from "@lib/config/constants";
import { sendTeamInviteEmail } from "@lib/emails/email-manager";
import { TeamInvite } from "@lib/emails/templates/team-invite-email";
import prisma from "@lib/prisma";
import slugify from "@lib/slugify";
@@ -90,7 +91,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
from: session.user.name,
to: usernameOrEmail,
teamName: team.name,
joinLink: `${BASE_URL}/auth/signup?token=${token}&callbackUrl=${BASE_URL + "/settings/teams"}`,
joinLink: `${BASE_URL}/auth/signup?token=${token}&callbackUrl=/settings/teams}`,
};
await sendTeamInviteEmail(teamInviteEvent);
+1 -1
View File
@@ -13,7 +13,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ message: "Not authenticated" });
}
if (!["GET", "POST"].includes(req.method!)) {
if (!["GET", "POST"].includes(req.method || "")) {
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
}
+19 -2
View File
@@ -36,9 +36,26 @@ const components = {
}}
renderItem={(item) =>
isTabletAndUp ? (
<Image src={item} alt="" loading="eager" layout="fixed" width={573} height={382} />
<Image
src={item}
alt=""
loading="eager"
layout="fixed"
objectFit="contain"
objectPosition={"center center"}
width={573}
height={382}
/>
) : (
<Image src={item} alt="" layout="responsive" width={573} height={382} />
<Image
src={item}
alt=""
layout="responsive"
objectFit="contain"
objectPosition={"center center"}
width={573}
height={382}
/>
)
}
/>
+117 -140
View File
@@ -1,21 +1,21 @@
import { ArrowRightIcon, ViewGridIcon } from "@heroicons/react/solid";
import Image from "next/image";
import React, { useEffect, useState } from "react";
import { JSONObject } from "superjson/dist/types";
import React from "react";
import { AppConfiguration, InstallAppButton } from "@calcom/app-store/components";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { App } from "@calcom/types/App";
import type { App } from "@calcom/types/App";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import EmptyScreen from "@calcom/ui/EmptyScreen";
import { QueryCell } from "@lib/QueryCell";
import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error";
import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
import AppsShell from "@components/AppsShell";
import { ClientSuspense } from "@components/ClientSuspense";
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
import Shell, { ShellSubHeading } from "@components/Shell";
import SkeletonLoader from "@components/apps/SkeletonLoader";
@@ -24,10 +24,8 @@ import DisconnectIntegration from "@components/integrations/DisconnectIntegratio
import DisconnectStripeIntegration from "@components/integrations/DisconnectStripeIntegration";
import IntegrationListItem from "@components/integrations/IntegrationListItem";
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
import WebhookListContainer from "@components/webhook/WebhookListContainer";
function ConnectOrDisconnectIntegrationButton(props: {
//
credentialIds: number[];
type: App["type"];
isGlobal?: boolean;
@@ -95,122 +93,91 @@ function ConnectOrDisconnectIntegrationButton(props: {
);
}
function IntegrationsContainer() {
interface IntegrationsContainerProps {
variant: App["variant"];
className?: string;
}
const IntegrationsContainer = ({ variant, className = "" }: IntegrationsContainerProps): JSX.Element => {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations"], { suspense: true });
const query = trpc.useQuery(["viewer.integrations", { variant, onlyInstalled: true }], { suspense: true });
return (
<QueryCell
query={query}
success={({ data }) => (
<>
<ShellSubHeading
title={
<SubHeadingTitleWithConnections
title={t("conferencing")}
numConnections={data.conferencing.numActive}
/>
}
/>
<List>
{data.conferencing.items.map((item) => (
<IntegrationListItem
key={item.title}
title={item.title}
imageSrc={item.imageSrc}
description={item.description}
actions={
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed
/>
}
/>
))}
</List>
<ShellSubHeading
className="mt-10"
title={
<SubHeadingTitleWithConnections title={t("payment")} numConnections={data.payment.numActive} />
}
/>
<List>
{data.payment.items.map((item) => (
<IntegrationListItem
key={item.title}
imageSrc={item.imageSrc}
title={item.title}
description={item.description}
actions={
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed={item.installed}
/>
}
/>
))}
</List>
<ShellSubHeading
className="mt-10"
title={
<SubHeadingTitleWithConnections title={"Others"} numConnections={data?.other?.numActive || 0} />
}
/>
<List>
{data.other.items.map((item) => (
<IntegrationListItem
key={item.title}
imageSrc={item.imageSrc}
title={item.title}
description={item.description}
actions={
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed={item.installed}
/>
}>
<AppConfiguration type={item.type} credentialIds={item.credentialIds} />
</IntegrationListItem>
))}
</List>
</>
)}></QueryCell>
customLoader={<SkeletonLoader className={className} />}
success={({ data }) => {
return (
<>
{data.items.length > 0 && (
<div className={className}>
<ShellSubHeading
title={
<SubHeadingTitleWithConnections title={t(variant)} numConnections={data.items.length} />
}
/>
<List>
{data.items.map((item) => (
<IntegrationListItem
key={item.title}
title={item.title}
imageSrc={item.imageSrc}
description={item.description}
actions={
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
isGlobal={item.isGlobal}
installed
/>
}
/>
))}
</List>
</div>
)}
</>
);
}}
/>
);
}
};
function Web3Container() {
const { t } = useLocale();
const result = trpc.useQuery(["viewer.web3Integration"]);
const isWeb3Active = !!result.data?.isWeb3Active;
return (
<>
<ShellSubHeading title="Web3" subtitle={t("meet_people_with_the_same_tokens")} className="mt-10" />
<div className="lg:col-span-9 lg:pb-8">
<List>
<ListItem className={classNames("flex-col")}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3")}>
<Image width={40} height={40} src="/apps/metamask.svg" alt="Embed" />
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">
MetaMask (
<a className="text-blue-500" target="_blank" href="https://cal.com/web3" rel="noreferrer">
Read more
</a>
)
</ListItemTitle>
<ListItemText component="p">{t("only_book_people_and_allow")}</ListItemText>
</div>
<Web3ConnectBtn />
</div>
</ListItem>
</List>
</div>
{isWeb3Active && (
<>
<ShellSubHeading title="Web3" subtitle={t("meet_people_with_the_same_tokens")} className="mt-10" />
<div className="lg:col-span-9 lg:pb-8">
<List>
<ListItem className={classNames("flex-col")}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3")}>
<Image width={40} height={40} src="/apps/metamask.svg" alt="Embed" />
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3">
MetaMask (
<a
className="text-blue-500"
target="_blank"
href="https://cal.com/web3"
rel="noreferrer">
Read more
</a>
)
</ListItemTitle>
<ListItemText component="p">{t("only_book_people_and_allow")}</ListItemText>
</div>
<Web3ConnectBtn />
</div>
</ListItem>
</List>
</div>
</>
)}
</>
);
}
@@ -218,17 +185,16 @@ function Web3Container() {
function Web3ConnectBtn() {
const { t } = useLocale();
const utils = trpc.useContext();
const [connectionBtn, setConnection] = useState(false);
const result = trpc.useQuery(["viewer.web3Integration"]);
const mutation = trpc.useMutation("viewer.enableOrDisableWeb3", {
onSuccess: async (result) => {
const { key = {} } = result as JSONObject;
if ((key as JSONObject).isWeb3Active) {
const { key } = result;
if ((key as { isWeb3Active: boolean }).isWeb3Active) {
showToast(t("web3_metamask_added"), "success");
} else {
showToast(t("web3_metamask_disconnected"), "success");
}
utils.invalidateQueries("viewer.web3Integration");
},
onError: (err) => {
if (err instanceof HttpError) {
@@ -238,33 +204,23 @@ function Web3ConnectBtn() {
},
});
useEffect(() => {
if (result.data) {
setConnection(result.data.isWeb3Active as boolean);
}
}, [result]);
const enableOrDisableWeb3 = async (mutation: any) => {
const result = await mutation.mutateAsync({});
setConnection(result.key.isWeb3Active);
utils.invalidateQueries("viewer.web3Integration");
};
return (
<Button
loading={mutation.isLoading}
color={connectionBtn ? "warn" : "secondary"}
color={result.data?.isWeb3Active ? "warn" : "secondary"}
disabled={result.isLoading || mutation.isLoading}
onClick={async () => await enableOrDisableWeb3(mutation)}
onClick={() => {
mutation.mutateAsync({});
}}
data-testid="metamask">
{connectionBtn ? t("remove") : t("add")}
{result.data?.isWeb3Active ? t("remove") : t("add")}
</Button>
);
}
export default function IntegrationsPage() {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations", { onlyInstalled: true }]);
return (
<Shell
heading={t("installed_apps")}
@@ -272,12 +228,33 @@ export default function IntegrationsPage() {
large
customLoader={<SkeletonLoader />}>
<AppsShell>
<ClientSuspense fallback={<SkeletonLoader />}>
<IntegrationsContainer />
<CalendarListContainer />
<WebhookListContainer title={t("webhooks")} subtitle={t("receive_cal_meeting_data")} />
<Web3Container />
</ClientSuspense>
<QueryCell
query={query}
success={({ data }) => {
return data.items.length > 0 ? (
<>
<IntegrationsContainer variant="conferencing" />
<CalendarListContainer />
<IntegrationsContainer variant="payment" className="mt-8" />
<IntegrationsContainer variant="other" className="mt-8" />
<Web3Container />
</>
) : (
<EmptyScreen
Icon={ViewGridIcon}
headline={t("empty_installed_apps_headline")}
description={
<>
<span className="mb-6 block">{t("empty_installed_apps_description")}</span>
<Button href="/apps" EndIcon={ArrowRightIcon}>
{t("empty_installed_apps_button")}
</Button>
</>
}
/>
);
}}
/>
</AppsShell>
</Shell>
);
+2 -4
View File
@@ -107,7 +107,7 @@ export default function Login({
className="space-y-6"
handleSubmit={async (values) => {
setErrorMessage(null);
telemetry.withJitsu((jitsu) => jitsu.track(telemetryEventTypes.login, collectPageParameters()));
telemetry.event(telemetryEventTypes.login, collectPageParameters());
const res = await signIn<"credentials">("credentials", {
...values,
callbackUrl,
@@ -177,9 +177,7 @@ export default function Login({
onClick={async (e) => {
e.preventDefault();
// track Google logins. Without personal data/payload
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.googleLogin, collectPageParameters())
);
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
await signIn("google");
}}>
{t("signin_with_google")}
+1 -1
View File
@@ -2,12 +2,12 @@ import { ClockIcon } from "@heroicons/react/outline";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import EmptyScreen from "@calcom/ui/EmptyScreen";
import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import { inferQueryOutput, trpc } from "@lib/trpc";
import EmptyScreen from "@components/EmptyScreen";
import Shell from "@components/Shell";
import { NewScheduleButton } from "@components/availability/NewScheduleButton";
import { ScheduleListItem } from "@components/availability/ScheduleListItem";
+1 -1
View File
@@ -6,12 +6,12 @@ import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/componen
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import EmptyScreen from "@calcom/ui/EmptyScreen";
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
import BookingsShell from "@components/BookingsShell";
import EmptyScreen from "@components/EmptyScreen";
import Shell from "@components/Shell";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
+1 -3
View File
@@ -114,9 +114,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
reason: cancellationReason,
};
telemetry.withJitsu((jitsu) =>
jitsu.track(telemetryEventTypes.bookingCancelled, collectPageParameters())
);
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
const res = await fetch("/api/cancel", {
body: JSON.stringify(payload),
+5 -5
View File
@@ -6,6 +6,7 @@ import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RecurringEvent } from "@calcom/types/Calendar";
import { asStringOrThrow, asStringOrNull } from "@lib/asStringOrNull";
@@ -14,7 +15,6 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage";
import { getTranslation } from "@server/lib/i18n";
import { ssrInit } from "@server/lib/ssr";
dayjs.extend(utc);
@@ -23,7 +23,10 @@ dayjs.extend(timezone);
export type HashLinkPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: HashLinkPageProps) {
return <BookingPage {...props} />;
const { t } = useLocale();
const locationLabels = getLocationLabels(t);
return <BookingPage {...props} locationLabels={locationLabels} />;
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
@@ -150,8 +153,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
eventName: null,
};
const t = await getTranslation(context.locale ?? "en", "common");
// Checking if number of recurring event ocurrances is valid against event type configuration
const recurringEventCount =
(eventTypeObject?.recurringEvent?.count &&
@@ -163,7 +164,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
locationLabels: getLocationLabels(t),
profile,
eventType: eventTypeObject,
booking: null,
+13 -1
View File
@@ -328,6 +328,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const minSeats = 2;
const [enableSeats, setEnableSeats] = useState(!!eventType.seatsPerTimeSlot);
const [displayNameTips, setDisplayNameTips] = useState(false);
const periodType =
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
PERIOD_TYPES.find((s) => s.type === "UNLIMITED");
@@ -863,7 +864,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const membership = team?.members.find((membership) => membership.user.id === props.session.user.id);
const isAdmin = membership?.role === MembershipRole.OWNER || membership?.role === MembershipRole.ADMIN;
return (
<div>
<Shell
@@ -1188,8 +1188,20 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
className="block w-full rounded-sm border-gray-300 text-sm "
placeholder={t("meeting_with_user")}
defaultValue={eventType.eventName || ""}
onFocus={() => setDisplayNameTips(true)}
{...formMethods.register("eventName")}
onBlur={() => setDisplayNameTips(false)}
/>
{displayNameTips && (
<div className="mt-1 text-gray-500">
<p>{`{HOST} = ${t("your_name")}`}</p>
<p>{`{ATTENDEE} = ${t("attendee_name")}`}</p>
<p>{`{HOST/ATTENDEE} = ${t(
"dynamically_display_attendee_or_organizer"
)}`}</p>
<p>{`{LOCATION} = ${t("event_location")}`}</p>
</div>
)}
</div>
</div>
</div>
+1 -1
View File
@@ -31,6 +31,7 @@ import Dropdown, {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@calcom/ui/Dropdown";
import EmptyScreen from "@calcom/ui/EmptyScreen";
import { Tooltip } from "@calcom/ui/Tooltip";
import { withQuery } from "@lib/QueryCell";
@@ -39,7 +40,6 @@ import { HttpError } from "@lib/core/http/error";
import { inferQueryOutput, trpc } from "@lib/trpc";
import { EmbedButton, EmbedDialog } from "@components/Embed";
import EmptyScreen from "@components/EmptyScreen";
import Shell from "@components/Shell";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import CreateEventTypeButton from "@components/eventtype/CreateEventType";

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