Compare commits

...
103 Commits
Author SHA1 Message Date
zomars 5d775bd4a3 v2.3.6
release.yml / release (push) Canceled after 0s
2022-12-07 14:02:24 -07:00
Nafees Nazik 4e02b22472 fix: add optimistic update for disabling forms (#5547) 2022-12-07 13:55:41 -07:00
4686d3ff48 Improve multilingualism and fix search routes (#5334)
* fixed search routes

* made static text multilingual and fixed german translations

# Conflicts:
#	apps/web/components/availability/Schedule.tsx

* delete empty file created by fixing merge conflicts

* fixing TextField placeholder by passing attendeeName and fixed json variable

* Using useLocal to modify static text to make it multilingual, and passing the correct variables for brand and mail

* seperated whitelabel and improved translations

* added missing translation

* added missing translation for webhooks

* Updated AdminAppsView with dynamic translations.
Added translations for german and english files only.

* changed back to one liner

* updated german and english translations for impersonation.tsx

* updated german and english translations for impersonation.tsx and users.tsx. added missing german translation for timeformat_profile_hint

* updated german and english translations for team-billing-view.tsx

* updated german and english translations for LicenseRequired.tsx

* updated routes for profile and avatar

* yarn.lock updated from newer changes

* Revert " yarn.lock updated from newer changes"

This reverts commit efd9a90bf774371a331861e4fb441ab4f4d0b7fd.

* sanitize dangerouslySetInnerHTML to prevent xss attacks

* tried to fix window title flicker

* changed ssdInit to ssrInit for getServerSideProps. Serverside translation works but current route still set as a window title

* flicker with route in window title is caused here. It is not necessary to check if isPublic and session is false because it already gets checked in useRedirectToLoginIfUnauthenticated hook.

* fixed window title translation flicker for availability page

* fixed window title translation flicker for teams page

* fixed window title translation flicker for workflow page

* fixed error that div may not be rendered within p element

* fixed window title translation flicker for booking page

* fixed window title translation flicker by adding getServerSideProps

* Only set HeadSeo if an page title exists. If window title is set by the Meta component, shell is causing a flicker because it overwrites the title which is set by Meta. It is a problem especially for settings pages.

* fixed window title translation flicker by adding the Meta component to the skeleton

* fixed condition

* removed condition and added withoutSeo for settings pages

* using translations for create team page further fixed flicker for window title

* fixed flicker for window title for event-type creation page

* fixed flicker for window title for availability creation page

* fixed flicker for window title for sso page

* updated conferencing en translation

* added meta.CTA back it was mistakenly removed

* fixed flicker for workflows page

* fixed missing variable

* Update packages/ui/v2/core/Shell.tsx

* Delete index.tsx

* Update sso.tsx

* Updates subdmoules

Co-authored-by: maxi <maximilian.oehrlein@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-12-07 13:53:44 -07:00
zomars 02e9d8d45f Sync submodules 2022-12-07 13:40:37 -07:00
Udit Takkar 7ce5096f13 fix: resetting input on cancel and refactoring input fiedl (#5522)
* fix: resetting input on cancel and refactoring input fiedl

* feat: add username field in /usersettings

* chore: remove unused

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

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-07 13:15:15 -07:00
Udit Takkar 83e51e9b9d fix: use trim in zod (#5915)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-07 13:12:53 -07:00
Udit TakkarandPeer Richelsen dca3278c40 fix: zoom video (#5910)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-07 13:12:26 -07:00
Udit Takkar 7501635fbf fix: delete use when 2fa is disabled (#5899)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-07 13:12:14 -07:00
Peer Richelsen 80c96ea5ac remove "Add bugs to Bug Hunting" workflow (#5901)
not needed anymore
2022-12-07 13:09:01 -07:00
Hariom BalharaandPeer Richelsen 8c001e0d34 Fix reschedule embed (#5861)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-07 13:08:34 -07:00
Joe Au-Yeungandalannnc bf275497a2 MS Teams use joinWebUrl from response body (#5886)
* Add joinWebUrl to event

* Remove console.log

Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-07 12:56:15 -07:00
6e351a4b20 fix/premium-username-flow (#5815)
* Removes user plan logic for stripe sub

* Update hardcoded price

* Removed checkoutSessionId metadata and fix for trpc query

* Remove or comment team billing unused code

* Add todo note

* Fix type checks

* Change function that was renamed

* Remove duplicated test

* remove plan when creating user on test

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-07 12:55:47 -07:00
github-actions[bot]andCrowdin Bot 5360cea3bf New Crowdin translations by Github Action (#5919)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-07 20:49:45 +01:00
+2 2f36985462 /availability - Add ability to set as default in the dropdown (#5717)
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: gGh00sTt <coolmagnas@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: gGh00sTt <coolmagnas@gmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2022-12-07 12:35:55 -07:00
6b26e9a7de Harsh/fix timeformat hint (#5918)
* fix: 404 page for undefined events

* remove prev code merge

* fix: timeformat profile hint

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <root@pop-os.localdomain>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-07 18:22:43 +00:00
github-actions[bot]andCrowdin Bot d7655f0521 New Crowdin translations by Github Action (#5914)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-07 19:10:15 +01:00
sean-brydonandLeo Giovanetti f4ed345a87 Fixes disable branding for teams and users (#5913)
* Adding new nextauth object

* Use correct brand hidden values

* Move check to getprops instead of a function

* Update apps/web/components/booking/pages/AvailabilityPage.tsx

* Update apps/web/components/booking/pages/AvailabilityPage.tsx

* Update apps/web/pages/[user]/[type].tsx

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

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

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

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

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

Co-authored-by: Leo Giovanetti <hello@leog.me>
2022-12-07 15:04:04 +00:00
SumedhandPeer Richelsen c3122c82e4 fix/cant-create-team-name-taken (#5911)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-07 14:50:54 +00:00
Udit Takkar cc9b49131e fix: add toast and fix delete action (#5912)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-07 15:37:20 +01:00
github-actions[bot]andCrowdin Bot 190db1a0f6 New Crowdin translations by Github Action (#5903)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-06 23:25:53 +01:00
Cory Dransfeldt bd48a72e99 [Feature] Adds support for Plausible analytics to app store (#5906) 2022-12-06 21:19:16 +00:00
Nafees Nazik a6836a1488 fix: use the new docker compose command if it exists (#5852) 2022-12-06 11:41:50 -07:00
2b67ea9635 Harsh/fix 404 page for undefined events (#5700)
* fix: 404 page for undefined events

* Update apps/web/pages/[user]/[type].tsx

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

Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-12-06 15:04:34 +00:00
Udit Takkar a9117c2d36 fix: use tooltip portal (#5889)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-06 13:57:02 +00:00
+1 10d3b5f1b9 Remove the back arrow (#5402)
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Matheus Benini <matheus_benini@hotmail.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Matheus Benini <matheus_benini@hotmail.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
2022-12-06 13:02:58 +00:00
Peer Richelsen b757cb6ba1 added new teams empty screen (#5847)
* added new teams empty screen

* replaced svg background with smaller jpg

* nit

* nit

* nit

* nit

* show old emptyscreen for self-hosters

* nit
2022-12-05 15:20:33 -07:00
5968e0aa19 added EventReservationSchema to success page (#5777)
* added EventReservationSchema to success page

* removed duplicated of yarn.lock

Co-authored-by: René Müller <rene.mueller@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 21:35:44 +00:00
zomars a06fffc4ec Adds missing translation 2022-12-05 14:24:09 -07:00
zomars 3312c84e7c Update turbo.json 2022-12-05 14:17:35 -07:00
Nafees NazikandPeer Richelsen d86710d5e9 fix: remove unnecessary title (#5853)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 21:11:10 +00:00
Udit Takkar 2d8e65c160 fix: ui of calendar item (#5879)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-12-05 20:39:52 +00:00
Udit TakkarandPeer Richelsen f74eaf7cb3 fix: increase edit location height (#5882)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 20:18:45 +00:00
bcfd57c953 Bugfix/payment options (#5805)
* removed locale to use automatic detection in teams/lib/payments.ts

* removed payment_method_types to manage payment methods in the Dashboard

* added customer_update and automatic_tax to apply tax rates to payments

* added allow_promotion_codes to allow promotion codes

* removed multiple usages of `allow_promotion_codes` in payments.ts

Co-authored-by: René Müller <rene.mueller@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-05 13:02:59 -07:00
c38588b93d New Crowdin translations by Github Action (#5842)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 20:59:12 +01:00
alannnc 32ed43574d Fix input parse to number (#5884) 2022-12-05 20:57:33 +01:00
38a5a22b9e Harsh/fix bookings name (#5848)
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <root@pop-os.localdomain>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 17:41:40 +00:00
Sascha FoersterandPeer Richelsen 2120e301c3 Update german translations (#5854)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 17:29:19 +00:00
Bailey Pumfleet dd0aec4e97 Fix Kbar translation (#5867) 2022-12-05 14:43:31 +00:00
Peer Richelsen 3815b86258 added norwegian to i18n (#5866) 2022-12-05 13:58:00 +00:00
Hariom BalharaandPeer Richelsen 7a6545bd03 Wordpress App Files cleanup (#5858)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 13:42:38 +00:00
Udit TakkarandPeer Richelsen 11f467af5d fix: toggle (#5865)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-05 13:28:21 +00:00
0930cf50b6 BugFix: Link based Apps installation - Opens 2 tabs instead of 1 (#5859)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-05 14:13:01 +01:00
c37cddd0b7 feat: add each app from the app store to KBar (#5724)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-05 12:13:31 +00:00
09253f83bf [CAL-463] Clicking Members from event types screen should take you to the members page (#5857)
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Matheus Benini <matheus_benini@hotmail.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-12-05 12:12:37 +00:00
Leo GiovanettiandBailey Pumfleet 1cdcebac01 Requires Confirmation Threshold (#5825)
* WIP + react errors fixed

* Booking and Availability page treatment

* Update BookingPage.tsx

Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-12-05 12:12:14 +00:00
sean-brydon 58270fa653 Team impersonation (#5863)
* Re Implement frontend logic to impersonate team members

* Refactor dialog + correct disable toggle.

* fix translation

* Update packages/features/ee/impersonation/lib/ImpersonationProvider.ts
2022-12-05 12:03:36 +00:00
Peer Richelsen 1a26a254ab added team subdomain to whereby validation (#5850) 2022-12-03 19:32:40 +00:00
Udit Takkarandalannnc a36d15894d fix: user infer props and remove avatarSSr from onboarding (#5549)
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-03 19:20:18 +00:00
Sumedh cf077fd33c fix/sidebar-profiledropdown-wired-hoverstate (#5832) 2022-12-03 15:12:12 +00:00
sean-brydonandPeer Richelsen 0c979d3604 Fix imports + buttons (#5831)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-03 14:53:07 +00:00
zomars 33bc786bb3 API scripts 2022-12-02 15:25:13 -07:00
zomars a7f16462db v2.3.5
release.yml / release (push) Canceled after 0s
2022-12-02 13:33:41 -07:00
zomars 2c7fcbcf51 hotfix: 404 for mixed case usernames 2022-12-02 13:32:56 -07:00
Syed Ali ShahbazandAlex van Andel d3e4d26fa5 fixes UTC consideration in CalDAV (#5827)
* fixes UTC consideration

* typecheck fix

* code quality

* comment location

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-02 19:46:38 +00:00
Carina WollendorferandCarinaWolli 3be6c05722 Fixes some translation issues (#5841)
* fix french translation for rescheduled email

* fix translations for team invite

* fix rescheduling email in german

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-02 17:56:48 +00:00
Carina WollendorferandCarinaWolli e293e590bd remove if and at to query (#5838)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-02 13:43:07 +00:00
+1 d8c1c103a1 [CAL-103] All Bookings show up under Today and Today should be uppercase (#5155)
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Rafael <rafael.toledo@engenharia.ufjf.br>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-02 10:08:12 +00:00
Joe Au-YeungandAlan 19d00e691c Fix attendees on a booking with seats and payments, navigate to the payment page. (#5788)
* Payment booking logic to seats

* Pass email to success page from payment page

* Fix type error

Co-authored-by: Alan <alannnc@gmail.com>
2022-12-02 00:12:06 +00:00
78aa4aecef Settings: Tab item line height fix && Stop x-axis overflow of settings tab (#5699)
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-01 23:09:14 +00:00
3ab002e547 5444 cal 339 radio option in additional questions on public booking page (#5804)
* Use field array intro

* WIP - form submitting wrong form

* WIP with fake useFormHook

* WORKING! OMG

Co-authored-by: Alex <alex@cal.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>

* Booking Page styling

* Fix duplicate fields

* Radio string

* Type error

* Linting errors

* Remove unused duplicate file

* Fixed user related type error

* remove log

* Remove console logs

* remove console log

* fix dark mode text and comment style

Co-authored-by: Alex <alex@cal.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-01 21:53:52 +00:00
Nafees Nazikandalannnc 7f461bc275 fix: padding (#5808)
Co-authored-by: alannnc <alannnc@gmail.com>
2022-12-01 13:30:57 -07:00
Carina WollendorferandCarinaWolli 571176102e add smsReminderNumber to webhook payload (#5823)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-01 12:51:59 -07:00
github-actions[bot]andCrowdin Bot 8f1d3fbc50 New Crowdin translations by Github Action (#5816)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-01 15:42:24 +00:00
Joe Au-YeungandAlex van Andel 5e88ad50ea If no location is set, default to Cal Video (#5812)
* Default to Cal Video

* Show web conferencing details to follow on success page

* Show web conferencing details to follow on success page

* Show web conferencing details to follow on success page

* Type fixes

* Pass tests

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-01 10:20:01 -05:00
Alex van Andel 056406bdd9 v2.3.4
release.yml / release (push) Canceled after 0s
2022-12-01 11:18:18 +00:00
Alex van AndelandOmar López 58af5a18a4 Moved some files around & made some fixes (#5811)
* Moved some files around & made some fixes

* Update apps/web/pages/event-types/index.tsx

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

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

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

* Update apps/web/components/apps/App.tsx

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

* Update apps/web/components/apps/CalendarListContainer.tsx

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

* Update apps/web/pages/settings/my-account/calendars.tsx

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

Co-authored-by: Omar López <zomars@me.com>
2022-12-01 00:59:27 +00:00
zomars 90b6581d76 Allows promo codes for team subscriptions 2022-11-30 17:36:04 -07:00
sean-brydon b33dcc8841 Weird (#5818) 2022-11-30 17:11:59 -07:00
d951a5b872 Allows brand customization (#5329)
* adjustments for each language json file:
- changed every Cal or Cal.com with a variable to make it possible to change that with a custom brand
- fix and renamed  ATTENDEE with attendeeName

* added two new variables for appName and support mail address. so everybody can change it via env

* changed static Cal or Cal.com with new defined constants

* Using useLocal to modify static text to make it multilingual, and passing the correct variables for brand and mail

* adding new readable variables for brand, website domain and mail address

* fixed search routes

* made static text multilingual and fixed german translations

* Revert "fixed search routes"
moved changes in another pr
This reverts commit e6ba11a1ec7821d8c16c502d0357f6d5fcdb1958.

* revert non whitelabel changes and moved it into another pr

* revert attendeeName fix

* reverted translation fixes and moved them in another pr

* changed back to "Cal.com Logo"

* changed back to "https://console.cal.com"

* added new env variable for company name and replaced some domainName variables in language files

* changed default for COMPANY_NAME to Cal.com, Inc.

* changed Cal.com to APP_NAME for mail templates

* Dropped website domain in favor of app name

* Update .env.example

* Apply suggestions from code review

* Code review feedback

* Delete App.tsx

* Update packages/ui/Kbar.tsx

* added meta.CTA back it was mistakenly removed

* updated add members test

Co-authored-by: maxi <maximilian.oehrlein@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-11-30 14:52:56 -07:00
Nafees NazikandPeer Richelsen 7122a9cfbf fix: padding (#5809)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-30 21:41:29 +00:00
f1b92c49fc App Store Search (#5704)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2022-11-30 20:51:44 +00:00
Leo Giovanettiandkodiakhq[bot] ef3ad36c2c Fixed listing formatting and save for custom inputs (#5795)
* Fixed formatting and save

* Additional tweaks

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-30 11:53:28 -07:00
072ce452b7 Catch the error that's thrown when no api_key is found (no daily) (#5696)
* fix/event-type-location-select (#5692)

* Fixed dialog components usage

* Fix styles

* Catch the error that's thrown when no api_key is found (no daily install)

Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-30 16:44:20 +00:00
Leo Giovanetti 0e0940f5f0 Fixing button/select styles (#5810) 2022-11-30 16:13:20 +00:00
Jeroen Reumkens 310936125d Changed select export to use v2 instead of v1. (#5715) 2022-11-30 08:42:45 +00:00
Peer Richelsen a9df3309b9 added wordpress plugin (#5168) 2022-11-29 21:57:26 +00:00
148d658f14 fix: update new team cancel button url (#5520)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-29 21:38:54 +00:00
Nafees NazikandPeer Richelsen 242d571d7d fix: missing rounded corners in team member settings (#5789)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-29 21:26:01 +00:00
dff49ec28a Rename /success to /manage and fix link in calendar event (#5719)
* fix need to make changes link in calendar event

* change /success link to /manage

* delete success.tsx file and use next.js rewrites

* fix e2e tests

* remove not needed waitForNavigation

* fix e2e tests

* rename manage?uid to booking/uid

* fix rewrite

* remove not needed export

* fix rescheduling e2e tests

* Minor fixes/cleannup

* Update BookingPage.tsx

* Moves cancel page to rewrite

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: zomars <zomars@me.com>
2022-11-29 20:27:29 +00:00
Leo Giovanetti 0c746ec10f Fixing add button (#5780) 2022-11-29 19:18:11 +00:00
Leo Giovanetti 901f54ca3a Fixing create event (#5781) 2022-11-29 11:47:08 -07:00
alannnc 16fda5f599 Enables require confirmation after payment its made (#5671) 2022-11-29 18:28:02 +00:00
d3e9adf382 allow cancelling and rescheduling for not confirmed bookings (#5770)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-11-29 16:03:39 +00:00
cf559e0726 New Crowdin translations by Github Action (#5753)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-11-29 15:51:24 +00:00
Udit Takkar 8447b1bbd5 fix: use shouldDirty flag (#5775)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2022-11-29 15:49:02 +00:00
Alex van Andel 0d03006653 v2.3.3
release.yml / release (push) Canceled after 0s
2022-11-29 09:40:40 +00:00
Alex van Andel d44c5fd8b0 Fixes availability to reset to default on save non-team (#5710) 2022-11-29 09:36:05 +00:00
checkly[bot] 74aa366f1e [Checkly] adds "Location Link" code (#5752)
Co-authored-by: checkly[bot] <54914329+checkly[bot]@users.noreply.github.com>
2022-11-28 16:52:24 -07:00
sean-brydon 7ebea57602 Dialog Api improvements (#5714) 2022-11-28 19:14:38 +00:00
26d675e162 Allow clients to choose different duration for a meeting (#5660)
* WIP

* Missing translation

* Default duration clears correctly

* Fixing hydration error

* Applying feedback

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-28 18:14:01 +00:00
Jeroen Reumkensandkodiakhq[bot] e16d29e916 Added optional chaining to embed iframe using window object, where objects were potentially undefined. (#5727)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-28 09:54:14 -07:00
zomars 0e89fb9f6a Update release.yml 2022-11-28 09:07:30 -07:00
zomars 43fb410a28 v2.3.2
release.yml / release (push) Canceled after 0s
2022-11-28 09:02:55 -07:00
b8bbfe8a3f fix: ui of reschedule dialog (#5691)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-11-28 08:52:42 -07:00
Leo GiovanettiandAlex van Andel 1f74fc394a Fixing calWarnings error on reschedule (#5711)
* Fixing calWarnings error on reschedule

* Update packages/core/CalendarManager.ts

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

* Update packages/core/CalendarManager.ts

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

* Going back on potential fix

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-11-28 15:43:24 +00:00
Carina WollendorferandCarinaWolli 9a12b5a0ac fix e2e tests (#5726)
* fix e2e tests

* remove not needed waitForNavigation

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-11-28 15:27:07 +00:00
Jeroen Reumkens 2ce2c1074b Don't return admin_min error when we're not validating for admin password. (#5713) 2022-11-28 07:57:02 -07:00
Nafees NazikandPeer Richelsen 0ef80da260 fix: remove unnecessary buttons from the image uploader modal (#5725)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-28 14:19:34 +00:00
github-actions[bot]andCrowdin Bot 1e50488cfe New Crowdin translations by Github Action (#5722)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-11-28 12:13:33 +00:00
sean-brydonandPeer Richelsen ae9f66c9ef InputField with suffix select (#5683)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-28 11:20:49 +00:00
798a4b0090 New Crowdin translations by Github Action (#5705)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-11-27 11:50:40 +00:00
Harsh SinghandGuest dcd9ff72b7 fix banner z-index (#5707)
* fix: 404 page for undefined events

* remove prev code merge

* change: z-index value of banner

Co-authored-by: Guest <guest@pop-os.localdomain>
2022-11-27 11:49:16 +00:00
GitStart 0b5a0186bc [CAL-280] mobile booking: selecting a day does not scroll first time (#5682) 2022-11-26 23:10:31 +00:00
283 changed files with 7564 additions and 4810 deletions
+5
View File
@@ -140,3 +140,8 @@ SENDGRID_SYNC_API_KEY=
# Sentry
NEXT_PUBLIC_SENTRY_DSN=
SENTRY_IGNORE_API_RESOLUTION_ERROR=
# Change your Brand
NEXT_PUBLIC_APP_NAME="Cal.com"
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
-22
View File
@@ -1,22 +0,0 @@
name: Add bugs to Bug Hunting
on:
issues:
types:
- opened
- labeled
pull_request:
types:
- opened
- labeled
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.1.0
with:
project-url: https://github.com/orgs/calcom/projects/9
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
labeled: 🙋🏻‍♂️help wanted
+1
View File
@@ -11,6 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
ref: main # Always checkout main even for tagged releases
fetch-depth: 0
token: ${{ secrets.GH_ACCESS_TOKEN }}
- run: git push origin +main:production
+35
View File
@@ -0,0 +1,35 @@
/**
* This is a Playwright script Checkly generated for you based on your Vercel project.
* To learn more about Browser checks and Playwright visit: https://www.checklyhq.com/docs/browser-checks
*/
// Create a Chromium browser
const { chromium } = require("playwright");
// Checkly supports top level await, but we wrap your code in an async function so you can run it locally too.
async function run() {
const browser = await chromium.launch();
const page = await browser.newPage();
const targetUrl = process.env.ENVIRONMENT_URL || "https://cal.com";
await page.goto(`${targetUrl}/rick/test-location-link`);
await page.waitForSelector('[data-testid="day"][data-disabled="false"]');
await page.click('[data-testid="day"][data-disabled="false"]');
await page.waitForSelector('[data-testid="time"]');
await page.click('[data-testid="time"]');
await page.waitForSelector("#name");
await page.click("#name");
await page.type("#name", "Calcom");
await page.type('[name="email"]', "cal@cal.com");
await page.waitForSelector('[data-testid="confirm-book-button"]');
await page.click('[data-testid="confirm-book-button"]');
await browser.close();
}
run();
+9 -10
View File
@@ -4,6 +4,7 @@ import { NextRouter, useRouter } from "next/router";
import { createRef, forwardRef, MutableRefObject, RefObject, useRef, useState } from "react";
import { components, ControlProps } from "react-select";
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import {
Button,
@@ -20,8 +21,6 @@ import {
TextField,
} from "@calcom/ui";
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
import ColorPicker from "@components/ui/colorpicker";
import Select from "@components/ui/form/Select";
@@ -498,7 +497,9 @@ const tabs = [
return (
<>
<div>
<small className="flex py-4 text-neutral-500">{t("place_where_cal_widget_appear")}</small>
<small className="flex py-4 text-neutral-500">
{t("place_where_cal_widget_appear", { appName: APP_NAME })}
</small>
</div>
<TextArea
data-testid="embed-code"
@@ -619,13 +620,13 @@ const ChooseEmbedTypesDialogContent = () => {
const { t } = useLocale();
const router = useRouter();
return (
<DialogContent type="creation" useOwnActionButtons size="lg">
<DialogContent type="creation" size="lg">
<div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{t("how_you_want_add_cal_site")}
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
</h3>
<div>
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site")}</p>
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site", { appName: APP_NAME })}</p>
</div>
</div>
<div className="flex items-start">
@@ -797,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
];
return (
<DialogContent size="xl" className="p-0.5" type="creation" useOwnActionButtons>
<DialogContent size="xl" className="p-0.5" type="creation">
<div className="flex">
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
@@ -1095,9 +1096,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
{t("copy_code")}
</Button>
) : null}
<DialogClose asChild>
<Button color="secondary">{t("Close")}</Button>
</DialogClose>
<DialogClose />
</div>
</div>
);
+2 -6
View File
@@ -156,12 +156,8 @@ export default function ImageUploader({
</div>
</div>
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
<DialogClose asChild>
<Button onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</Button>
</DialogClose>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<DialogClose onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</DialogClose>
<DialogClose color="secondary">{t("cancel")}</DialogClose>
</div>
</DialogContent>
</Dialog>
@@ -61,12 +61,10 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
<Select
name="additionalCalendar"
placeholder={
<div className="flex justify-start text-black">
<Icon.FiPlus className="-ml-1 h-4 w-4 ltr:mr-2 rtl:ml-2 rtl:-mr-1" />
<p>{t("add")}</p>
</div>
<Button StartIcon={Icon.FiPlus} color="secondary">
{t("add")}
</Button>
}
className="min-w-44"
options={options}
isSearchable={false}
isLoading={isLoading}
@@ -80,11 +78,16 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
width: "max-content",
right: "0",
}),
indicatorSeparator: (defaultStyles) => ({
...defaultStyles,
display: "none",
}),
control: (defaultStyles) => ({
...defaultStyles,
padding: "0",
border: "0",
borderRadius: "inherit",
borderRadius: "6px",
minHeight: "auto",
}),
dropdownIndicator: (defaultStyles) => ({
...defaultStyles,
+9 -13
View File
@@ -4,20 +4,14 @@ import React, { useState } from "react";
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import classNames from "@calcom/lib/classNames";
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App";
import {
Button,
DisconnectIntegration,
Icon,
Shell,
showToast,
SkeletonButton,
SkeletonText,
} from "@calcom/ui";
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
import HeadSeo from "@components/seo/head-seo";
@@ -48,7 +42,7 @@ const Component = ({
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
if (data.setupPending) return;
if (data?.setupPending) return;
showToast(t("app_successfully_installed"), "success");
},
onError: (error) => {
@@ -201,7 +195,7 @@ const Component = ({
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
<span>
{price === 0 ? (
"Free"
t("free_to_use_apps")
) : (
<>
{Intl.NumberFormat("en-US", {
@@ -279,8 +273,10 @@ const Component = ({
)}
</ul>
<hr className="my-8" />
<span className="leading-1 block text-xs text-gray-500">{t("every_app_published")}</span>
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
<span className="leading-1 block text-xs text-gray-500">
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
</span>
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
<Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")}
</a>
</div>
@@ -3,13 +3,13 @@ import Link from "next/link";
import { Fragment } from "react";
import { InstallAppButton } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Alert,
Button,
DisconnectIntegration,
EmptyScreen,
Icon,
List,
@@ -1,9 +1,12 @@
import { SchedulingType } from "@prisma/client";
import { FC, ReactNode } from "react";
import { FC, ReactNode, useEffect } from "react";
import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui";
import { Icon, Badge } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { UserAvatars } from "@components/booking/UserAvatars";
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
@@ -39,7 +42,35 @@ interface Props {
const BookingDescription: FC<Props> = (props) => {
const { profile, eventType, isBookingPage = false, children } = props;
const { date: bookingDate } = useRouterQuery("date");
const { t } = useLocale();
const { duration, setQuery: setDuration } = useRouterQuery("duration");
useEffect(() => {
if (eventType.metadata?.multipleDuration !== undefined) {
if (!duration) {
setDuration(eventType.length);
} else {
if (!eventType.metadata?.multipleDuration.includes(Number(duration))) {
setDuration(eventType.length);
}
}
}
}, []);
let requiresConfirmation = eventType?.requiresConfirmation;
let requiresConfirmationText = t("requires_confirmation");
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
if (rcThreshold) {
if (isBookingPage) {
if (dayjs(bookingDate).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) {
requiresConfirmation = false;
}
} else {
requiresConfirmationText = t("requires_confirmation_threshold", {
...rcThreshold,
unit: rcThreshold.unit.slice(0, -1),
});
}
}
return (
<>
<UserAvatars
@@ -75,29 +106,61 @@ const BookingDescription: FC<Props> = (props) => {
</div>
</div>
)}
{eventType?.requiresConfirmation && (
{requiresConfirmation && (
<div
className={classNames(
"flex items-center",
"items-top flex",
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}>
<div>
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] inline-block h-4 w-4 " />
</div>
{t("requires_confirmation")}
{requiresConfirmationText}
</div>
)}
<AvailableEventLocations
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
/>
<p
<div
className={classNames(
"text-sm font-medium",
"flex flex-nowrap text-sm font-medium",
isBookingPage && "dark:text-darkgray-600 text-gray-600"
)}>
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
{eventType.length} {t("minutes")}
</p>
<Icon.FiClock
className={classNames(
"min-h-4 min-w-4 mr-[10px] ml-[2px] inline-block",
isBookingPage && "mt-[2px]"
)}
/>
{eventType.metadata?.multipleDuration !== undefined ? (
!isBookingPage ? (
<ul className="-mt-1 flex flex-wrap gap-1">
{eventType.metadata.multipleDuration.map((dur, idx) => (
<li key={idx}>
<Badge
variant="gray"
size="lg"
className={classNames(
duration === dur.toString()
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-gray-200 text-gray-900 dark:text-white",
"cursor-pointer"
)}
onClick={() => {
setDuration(dur);
}}>
{dur} {t("minute_timeUnit")}
</Badge>
</li>
))}
</ul>
) : (
`${duration} ${t("minutes")}`
)
) : (
`${eventType.length} ${t("minutes")}`
)}
</div>
{children}
</div>
</>
@@ -118,7 +118,7 @@ function BookingListItem(booking: BookingItemProps) {
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
/* When cancelling we need to let the UI and the API know if the intention is to
cancel all remaining bookings or just that booking instance. */
href: `/success?uid=${booking.uid}&cancel=true${
href: `/booking/${booking.uid}?cancel=true${
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
}`,
icon: Icon.FiX,
@@ -195,11 +195,9 @@ function BookingListItem(booking: BookingItemProps) {
const onClickTableData = () => {
router.push({
pathname: "/success",
pathname: `/booking/${booking.uid}`,
query: {
uid: booking.uid,
allRemainingBookings: isTabRecurring,
listingStatus: booking.listingStatus,
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
},
});
@@ -237,9 +235,7 @@ function BookingListItem(booking: BookingItemProps) {
/>
<DialogFooter>
<DialogClose>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<DialogClose />
<Button
disabled={mutation.isLoading}
@@ -284,6 +280,11 @@ function BookingListItem(booking: BookingItemProps) {
{t("pending_payment")}
</Badge>
)}
{booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
{t("paid")}
</Badge>
)}
{recurringDates !== undefined && (
<div className="mt-2 text-sm text-gray-400">
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
@@ -1,7 +1,6 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventType } from "@prisma/client";
import * as Popover from "@radix-ui/react-popover";
import { TFunction } from "next-i18next";
import { useRouter } from "next/router";
import { useReducer, useEffect, useMemo, useState } from "react";
import { Toaster } from "react-hot-toast";
@@ -19,7 +18,6 @@ import {
} from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { WEBSITE_URL } from "@calcom/lib/constants";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
@@ -33,6 +31,7 @@ import { Icon, DatePicker } from "@calcom/ui";
import { timeZone as localStorageTimeZone } from "@lib/clock";
// import { timeZone } from "@lib/clock";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import Gates, { Gate, GateState } from "@components/Gates";
@@ -46,23 +45,6 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
// Get router variables
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
const router = useRouter();
const path = router.asPath.split("/");
path.pop(); // Remove the last item (where we currently are)
path.shift(); // Removes first item e.g. if we were visitng "/teams/test/30mins" the array will new look like ["teams","test"]
const slug = path.join("/");
return (
<div className="flex h-full flex-col justify-end">
<button title={t("profile")} onClick={() => router.replace(`${WEBSITE_URL}/${slug}`)}>
<Icon.FiArrowLeft className="dark:text-darkgray-600 h-4 w-4 text-black transition-opacity hover:cursor-pointer" />
<p className="sr-only">Go Back</p>
</button>
</div>
);
};
const useSlots = ({
eventTypeId,
eventTypeSlug,
@@ -70,6 +52,7 @@ const useSlots = ({
endTime,
usernameList,
timeZone,
duration,
}: {
eventTypeId: number;
eventTypeSlug: string;
@@ -77,6 +60,7 @@ const useSlots = ({
endTime?: Dayjs;
usernameList: string[];
timeZone?: string;
duration?: string;
}) => {
const { data, isLoading, isPaused } = trpc.viewer.public.slots.getSchedule.useQuery(
{
@@ -86,6 +70,7 @@ const useSlots = ({
startTime: startTime?.toISOString() || "",
endTime: endTime?.toISOString() || "",
timeZone,
duration,
},
{
enabled: !!startTime && !!endTime,
@@ -126,6 +111,7 @@ const SlotPicker = ({
}) => {
const [selectedDate, setSelectedDate] = useState<Dayjs>();
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
const { duration } = useRouterQuery("duration");
const { date, setQuery: setDate } = useRouterQuery("date");
const { month, setQuery: setMonth } = useRouterQuery("month");
const router = useRouter();
@@ -151,7 +137,7 @@ const SlotPicker = ({
setSelectedDate(dayjs.tz(date, timeZone));
}
}
}, [router.isReady, month, date, timeZone]);
}, [router.isReady, month, date, duration, timeZone]);
const { i18n, isLocaleReady } = useLocale();
const { slots: _1 } = useSlots({
@@ -161,6 +147,7 @@ const SlotPicker = ({
startTime: selectedDate?.startOf("day"),
endTime: selectedDate?.endOf("day"),
timeZone,
duration,
});
const { slots: _2, isLoading } = useSlots({
eventTypeId: eventType.id,
@@ -169,6 +156,7 @@ const SlotPicker = ({
startTime: browsingDate?.startOf("month"),
endTime: browsingDate?.endOf("month"),
timeZone,
duration,
});
const slots = useMemo(() => ({ ..._2, ..._1 }), [_1, _2]);
@@ -229,14 +217,14 @@ function TimezoneDropdown({
return (
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 inline-block rounded-md px-2 py-2 text-left text-gray-600">
<p className="text-sm font-medium">
<Icon.FiGlobe className="mr-[10px] ml-[2px] -mt-[2px] inline-block h-4 w-4" />
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
<p className="flex items-center text-sm font-medium">
<Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
{timeZone}
{isTimeOptionsOpen ? (
<Icon.FiChevronUp className="ml-1 inline-block h-4 w-4" />
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
) : (
<Icon.FiChevronDown className="ml-1 inline-block h-4 w-4" />
<Icon.FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
)}
</p>
</Popover.Trigger>
@@ -258,23 +246,6 @@ const dateQuerySchema = z.object({
timeZone: z.string().optional().default(""),
});
const useRouterQuery = <T extends string>(name: T) => {
const router = useRouter();
const existingQueryParams = router.asPath.split("?")[1];
const urlParams = new URLSearchParams(existingQueryParams);
const query = Object.fromEntries(urlParams);
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
};
return { [name]: query[name], setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
};
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
@@ -432,12 +403,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
{timezoneDropdown}
</BookingDescription>
{!isEmbed && (
<div className="mt-auto hidden md:block">
<GoBackToPreviousPage t={t} />
</div>
)}
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div>
<p
@@ -477,7 +442,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
/>
</div>
</div>
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
{(!restProps.isBrandingHidden || isEmbed) && <PoweredByCal />}
</div>
</main>
</div>
@@ -31,6 +31,7 @@ import {
} from "@calcom/embed-core/embed-iframe";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import { APP_NAME } from "@calcom/lib/constants";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
@@ -38,11 +39,12 @@ import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui";
import { Group, RadioField } from "@calcom/ui";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { ensureArray } from "@lib/ensureArray";
import useMeQuery from "@lib/hooks/useMeQuery";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import createBooking from "@lib/mutations/bookings/create-booking";
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
import { parseDate, parseRecurringDates } from "@lib/parseDate";
@@ -85,9 +87,7 @@ const BookingPage = ({
...restProps
}: BookingPageProps) => {
const { t, i18n } = useLocale();
// Get user so we can determine 12/24 hour format preferences
const query = useMeQuery();
const user = query.data;
const { duration: queryDuration } = useRouterQuery("duration");
const isEmbed = useIsEmbed(restProps.isEmbed);
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
@@ -103,6 +103,11 @@ const BookingPage = ({
{}
);
const stripeAppData = getStripeAppData(eventType);
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (queryDuration && !isNaN(Number(queryDuration))) {
duration = Number(queryDuration);
}
useEffect(() => {
if (top !== window) {
@@ -131,9 +136,8 @@ const BookingPage = ({
}
return router.push({
pathname: "/success",
pathname: `/booking/${uid}`,
query: {
uid,
isSuccessBookingPage: true,
email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug,
@@ -147,9 +151,8 @@ const BookingPage = ({
const { uid } = responseData[0] || {};
return router.push({
pathname: "/success",
pathname: `/booking/${uid}`,
query: {
uid,
allRemainingBookings: true,
email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug,
@@ -237,7 +240,7 @@ const BookingPage = ({
const bookingFormSchema = z
.object({
name: z.string().min(1),
email: z.string().email(),
email: z.string().trim().email(),
phone: z
.string()
.refine((val) => isValidPhoneNumber(val))
@@ -318,7 +321,7 @@ const BookingPage = ({
const recurringBookings = recurringDates.map((recurringDate) => ({
...booking,
start: dayjs(recurringDate).format(),
end: dayjs(recurringDate).add(eventType.length, "minute").format(),
end: dayjs(recurringDate).add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
recurringEventId,
@@ -351,7 +354,7 @@ const BookingPage = ({
mutation.mutate({
...booking,
start: dayjs(date).format(),
end: dayjs(date).add(eventType.length, "minute").format(),
end: dayjs(date).add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
timeZone: timeZone(),
@@ -426,7 +429,7 @@ const BookingPage = ({
eventTypeTitle: eventType.title,
profileName: profile.name,
})}{" "}
| Cal.com
| {APP_NAME}
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
@@ -741,6 +744,27 @@ const BookingPage = ({
</div>
</div>
)}
{input.options && input.type === EventTypeCustomInputType.RADIO && (
<div className="">
<div className="flex">
<Group
onValueChange={(e) => {
bookingForm.setValue(`customInputs.${input.id}`, e);
}}>
<>
{input.options.map((option, i) => (
<RadioField
label={option.label}
key={`option.${i}.radio`}
value={option.label}
id={`option.${i}.radio`}
/>
))}
</>
</Group>
</div>
</div>
)}
</div>
))}
{!eventType.disableGuests && guestToggle && (
@@ -839,7 +863,7 @@ const BookingPage = ({
) : (
<textarea
{...bookingForm.register("notes")}
required={!!eventType.metadata.additionalNotesRequired}
required={!!eventType.metadata?.additionalNotesRequired}
id="notes"
name="notes"
rows={3}
@@ -212,7 +212,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
return (
<Dialog open={isOpenDialog}>
<DialogContent useOwnActionButtons>
<DialogContent>
<div className="flex flex-row space-x-3">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
@@ -297,7 +297,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
control={locationFormMethods.control}
render={() => (
<Select<{ label: string; value: string; icon?: string }>
maxMenuHeight={150}
maxMenuHeight={300}
name="location"
defaultValue={selection}
options={locationOptions}
@@ -47,8 +47,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
</div>
<div className="pt-1">
<DialogHeader title={t("send_reschedule_request")} />
<p className="-mt-8 text-sm text-gray-500">{t("reschedule_modal_description")}</p>
<p className="text-sm text-gray-500">{t("reschedule_modal_description")}</p>
<p className="mt-6 mb-2 text-sm font-bold text-black">
{t("reason_for_reschedule_request")}
<span className="font-normal text-gray-500"> (Optional)</span>
@@ -62,9 +61,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
/>
<DialogFooter>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
<DialogClose />
<Button
data-testid="send_request"
disabled={isLoading}
@@ -20,12 +20,13 @@ type AvailabilityOption = {
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
const { t } = useLocale();
return (
<components.Option {...props}>
<span>{label}</span>
{isDefault && (
<Badge variant="blue" className="ml-2">
Default
{t("default")}
</Badge>
)}
</components.Option>
@@ -34,12 +35,13 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
const { label, isDefault } = props.data;
const { t } = useLocale();
return (
<components.SingleValue {...props}>
<span>{label}</span>
{isDefault && (
<Badge variant="blue" className="ml-2">
Default
{t("default")}
</Badge>
)}
</components.SingleValue>
@@ -57,6 +59,8 @@ const AvailabilitySelect = ({
onChange: (value: AvailabilityOption | null) => void;
}) => {
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
const { t } = useLocale();
if (isLoading) {
return <SelectSkeletonLoader />;
}
@@ -77,6 +81,7 @@ const AvailabilitySelect = ({
return (
<Select
placeholder={t("select")}
options={options}
isSearchable={false}
onChange={props.onChange}
@@ -1,352 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { SchedulingType } from "@prisma/client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import type { z } from "zod";
import classNames from "@calcom/lib/classNames";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
import { trpc } from "@calcom/trpc/react";
import {
Alert,
Button,
Dialog,
DialogClose,
DialogContent,
Dropdown,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
Form,
Icon,
InputLeading,
showToast,
TextAreaField,
TextField,
} from "@calcom/ui";
import { HttpError } from "@lib/core/http/error";
import { slugify } from "@lib/slugify";
import Avatar from "@components/ui/Avatar";
import * as RadioArea from "@components/ui/form/radio-area";
// this describes the uniform data needed to create a new event type on Profile or Team
export interface EventTypeParent {
teamId: number | null | undefined; // if undefined, then it's a profile
name?: string | null;
slug?: string | null;
image?: string | null;
}
interface CreateEventTypeBtnProps {
// set true for use on the team settings page
canAddEvents: boolean;
// set true when in use on the team settings page
isIndividualTeam?: boolean;
// EventTypeParent can be a profile (as first option) or a team for the rest.
options: EventTypeParent[];
}
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
const { t, isLocaleReady } = useLocale();
const router = useRouter();
// URL encoded params
const teamId: number | undefined =
typeof router.query.teamId === "string" && router.query.teamId
? parseInt(router.query.teamId)
: undefined;
const pageSlug = router.query.eventPage || props.options[0].slug;
const hasTeams = !!props.options.find((option) => option.teamId);
const type: string = typeof router.query.type == "string" && router.query.type ? router.query.type : "";
const form = useForm<z.infer<typeof createEventTypeInput>>({
resolver: zodResolver(createEventTypeInput),
});
const { setValue, watch, register } = form;
useEffect(() => {
if (!router.isReady) return;
const title: string =
typeof router.query.title === "string" && router.query.title ? router.query.title : "";
const length: number =
typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15;
const description: string =
typeof router.query.description === "string" && router.query.description
? router.query.description
: "";
const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : "";
setValue("title", title);
setValue("length", length);
setValue("description", description);
setValue("slug", slug);
// If query params change, update the form
}, [router.isReady, router.query, setValue]);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
if (name === "title" && type === "change") {
if (value.title) setValue("slug", slugify(value.title));
else setValue("slug", "");
}
});
return () => subscription.unsubscribe();
}, [watch, setValue]);
const createMutation = trpc.viewer.eventTypes.create.useMutation({
onSuccess: async ({ eventType }) => {
await router.replace("/event-types/" + eventType.id);
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
if (err.data?.code === "BAD_REQUEST") {
const message = `${err.data.code}: URL already exists.`;
showToast(message, "error");
}
if (err.data?.code === "UNAUTHORIZED") {
const message = `${err.data.code}: You are not able to create this event`;
showToast(message, "error");
}
},
});
// inject selection data into url for correct router history
const openModal = (option: EventTypeParent) => {
const query = {
...router.query,
dialog: "new-eventtype",
eventPage: option.slug,
teamId: option.teamId,
};
if (!option.teamId) {
delete query.teamId;
}
router.push(
{
pathname: router.pathname,
query,
},
undefined,
{ shallow: true }
);
};
if (!isLocaleReady) {
return null;
}
return (
<Dialog
name="new-eventtype"
clearQueryParamsOnClose={["eventPage", "teamId", "type", "description", "title", "length", "slug"]}>
<CreateEventTypeTrigger
hasTeams={hasTeams}
canAddEvents={props.canAddEvents}
isIndividualTeam={props.isIndividualTeam}
openModal={openModal}
options={props.options}
/>
<DialogContent>
<div className="mb-4">
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
</h3>
<div>
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
</div>
</div>
<Form
form={form}
handleSubmit={(values) => {
createMutation.mutate(values);
}}>
<div className="mt-3 space-y-4">
{teamId && (
<TextField
type="hidden"
labelProps={{ style: { display: "none" } }}
{...register("teamId", { valueAsNumber: true })}
value={teamId}
/>
)}
<TextField label={t("title")} placeholder={t("quick_chat")} {...register("title")} />
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
<TextField
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
required
addOnLeading={<InputLeading>/{pageSlug}/</InputLeading>}
{...register("slug")}
/>
) : (
<TextField
label={t("url")}
required
addOnLeading={
<InputLeading>
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
</InputLeading>
}
{...register("slug")}
/>
)}
<TextAreaField
label={t("description")}
placeholder={t("quick_video_meeting")}
{...register("description")}
/>
<div className="relative">
<TextField
type="number"
required
min="10"
placeholder="15"
label={t("length")}
className="pr-20"
{...register("length", { valueAsNumber: true })}
/>
<div className="absolute inset-y-0 right-0 mt-1.5 flex items-center pt-4 pr-3 text-sm text-gray-400">
{t("minutes")}
</div>
</div>
{teamId && (
<div className="mb-4">
<label htmlFor="schedulingType" className="block text-sm font-bold text-gray-700">
{t("scheduling_type")}
</label>
{form.formState.errors.schedulingType && (
<Alert
className="mt-1"
severity="error"
message={form.formState.errors.schedulingType.message}
/>
)}
<RadioArea.Group
{...register("schedulingType")}
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse">
<RadioArea.Item
value={SchedulingType.COLLECTIVE}
defaultChecked={type === SchedulingType.COLLECTIVE}
className="w-1/2 text-sm">
<strong className="mb-1 block">{t("collective")}</strong>
<p>{t("collective_description")}</p>
</RadioArea.Item>
<RadioArea.Item
value={SchedulingType.ROUND_ROBIN}
defaultChecked={type === SchedulingType.ROUND_ROBIN}
className="w-1/2 text-sm">
<strong className="mb-1 block">{t("round_robin")}</strong>
<p>{t("round_robin_description")}</p>
</RadioArea.Item>
</RadioArea.Group>
</div>
)}
</div>
<div className="mt-8 flex flex-row-reverse gap-x-2">
<Button type="submit" loading={createMutation.isLoading}>
{t("continue")}
</Button>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
</div>
</Form>
</DialogContent>
</Dialog>
);
}
type CreateEventTypeTrigger = {
isIndividualTeam?: boolean;
// EventTypeParent can be a profile (as first option) or a team for the rest.
options: EventTypeParent[];
hasTeams: boolean;
// set true for use on the team settings page
canAddEvents: boolean;
openModal: (option: EventTypeParent) => void;
};
export function CreateEventTypeTrigger(props: CreateEventTypeTrigger) {
const { t } = useLocale();
return (
<>
{!props.hasTeams || props.isIndividualTeam ? (
<Button
onClick={() => props.openModal(props.options[0])}
data-testid="new-event-type"
StartIcon={Icon.FiPlus}
disabled={!props.canAddEvents}>
{t("new_event_type_btn")}
</Button>
) : (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button EndIcon={Icon.FiChevronDown}>{t("new_event_type_btn")}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
<DropdownMenuSeparator className="h-px bg-gray-200" />
{props.options.map((option) => (
<CreateEventTeamsItem
key={option.slug}
option={option}
openModal={() => props.openModal(option)}
/>
))}
</DropdownMenuContent>
</Dropdown>
)}
</>
);
}
function CreateEventTeamsItem(props: {
openModal: (option: EventTypeParent) => void;
option: EventTypeParent;
}) {
const session = useSession();
const membershipQuery = trpc.viewer.teams.getMembershipbyUser.useQuery({
memberId: session.data?.user.id as number,
teamId: props.option.teamId as number,
});
const isDisabled = membershipQuery.data?.role === "MEMBER";
return (
<DropdownMenuItem
key={props.option.slug}
className={classNames(
"cursor-pointer px-3 py-2 focus:outline-none",
isDisabled ? "cursor-default !text-gray-300" : "hover:bg-neutral-100"
)}
disabled={isDisabled}
onSelect={() => props.openModal(props.option)}>
<Avatar
alt={props.option.name || ""}
imageSrc={props.option.image || WEBAPP_URL + "/" + props.option.slug + "/avatar.png"}
size={6}
className="inline ltr:mr-2 rtl:ml-2"
/>
{props.option.name ? props.option.name : props.option.slug}
</DropdownMenuItem>
);
}
@@ -1,23 +1,33 @@
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventTypeCustomInputType } from "@prisma/client";
import type { CustomInputParsed } from "pages/event-types/[type]";
import { FC } from "react";
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
import { Button, Select, TextField } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Label, Select, TextField } from "@calcom/ui";
interface OptionTypeBase {
label: string;
value: EventTypeCustomInputType;
options?: { label: string; type: string }[];
}
interface Props {
onSubmit: SubmitHandler<IFormInput>;
onSubmit: (output: CustomInputParsed) => void;
onCancel: () => void;
selectedCustomInput?: EventTypeCustomInput;
selectedCustomInput?: CustomInputParsed;
}
type IFormInput = EventTypeCustomInput;
type IFormInput = CustomInputParsed;
/**
* Getting a random ID gives us the option to know WHICH field is changed
* when the user edits a custom field.
* This UUID is only used to check for changes in the UI and not the ID we use in the DB
* There is very very very slim chance that this will cause a collision
* */
const randomId = () => Math.floor(Math.random() * 1000000 + new Date().getTime());
const CustomInputTypeForm: FC<Props> = (props) => {
const { t } = useLocale();
@@ -26,10 +36,22 @@ const CustomInputTypeForm: FC<Props> = (props) => {
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
{
value: EventTypeCustomInputType.RADIO,
label: t("radio"),
},
];
const { selectedCustomInput } = props;
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
const { register, control, handleSubmit } = useForm<IFormInput>({
const defaultValues = selectedCustomInput
? { ...selectedCustomInput, id: selectedCustomInput?.id || randomId() }
: {
id: randomId(),
type: EventTypeCustomInputType.TEXT,
};
const { register, control, getValues } = useForm<IFormInput>({
defaultValues,
});
const selectedInputType = useWatch({ name: "type", control });
@@ -40,7 +62,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
};
return (
<form onSubmit={handleSubmit(props.onSubmit)} className="flex flex-col space-y-4">
<div className="flex flex-col space-y-4">
<div>
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
{t("input_type")}
@@ -83,6 +105,10 @@ const CustomInputTypeForm: FC<Props> = (props) => {
{...register("placeholder")}
/>
)}
{selectedInputType === EventTypeCustomInputType.RADIO && (
<RadioInputHandler control={control} register={register} />
)}
<div className="flex h-5 items-center">
<input
id="required"
@@ -111,10 +137,75 @@ const CustomInputTypeForm: FC<Props> = (props) => {
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
{t("cancel")}
</Button>
<Button type="submit">{t("save")}</Button>
<Button
type="button"
onClick={() => {
props.onSubmit(getValues());
}}>
{t("save")}
</Button>
</div>
</form>
</div>
);
};
function RadioInputHandler({
register,
control,
}: {
register: UseFormRegister<IFormInput>;
control: Control<IFormInput>;
}) {
const { t } = useLocale();
const { fields, append, remove } = useFieldArray<IFormInput>({
control,
name: "options",
shouldUnregister: true,
});
const [animateRef] = useAutoAnimate<HTMLUListElement>();
return (
<div className="flex flex-col ">
<Label htmlFor="radio_options">{t("options")}</Label>
<ul
className="flex max-h-80 w-full flex-col space-y-1 overflow-y-scroll rounded-md bg-gray-50 p-4"
ref={animateRef}>
<>
{fields.map((option, index) => (
<li key={`${option.id}`}>
<TextField
id={option.id}
placeholder={t("enter_option", { index: index + 1 })}
addOnFilled={false}
label={t("option", { index: index + 1 })}
labelSrOnly
{...register(`options.${index}.label` as const, { required: true })}
addOnSuffix={
<Button
size="icon"
color="minimal"
StartIcon={Icon.FiX}
onClick={() => {
remove(index);
}}
/>
}
/>
</li>
))}
<Button
color="minimal"
StartIcon={Icon.FiPlus}
className="!text-sm !font-medium"
onClick={() => {
append({ label: "", type: "text" });
}}>
{t("add_an_option")}
</Button>
</>
</ul>
</div>
);
}
export default CustomInputTypeForm;
@@ -1,372 +0,0 @@
import { ErrorMessage } from "@hookform/error-message";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next";
import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { components } from "react-select";
import { z } from "zod";
import {
EventLocationType,
getEventLocationType,
getHumanReadableLocationValue,
getMessageForOrganizer,
LocationObject,
LocationType,
} from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, Form, Icon, Label, PhoneInput, Select } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
import { LinkText } from "@components/ui/LinkText";
import CheckboxField from "@components/ui/form/CheckboxField";
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
type OptionTypeBase = {
label: string;
value: EventLocationType["type"];
disabled?: boolean;
};
interface ISetLocationDialog {
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: OptionTypeBase;
booking?: BookingItem;
defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
}
const LocationInput = (props: {
eventLocationType: EventLocationType;
locationFormMethods: ReturnType<typeof useForm>;
id: string;
required: boolean;
placeholder: string;
className?: string;
defaultValue?: string;
}): JSX.Element | null => {
const { eventLocationType, locationFormMethods, ...remainingProps } = props;
if (eventLocationType?.organizerInputType === "text") {
return (
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
);
} else if (eventLocationType?.organizerInputType === "phone") {
return (
<PhoneInput
name={eventLocationType.variable}
control={locationFormMethods.control}
{...remainingProps}
/>
);
}
return null;
};
export const EditLocationDialog = (props: ISetLocationDialog) => {
const {
saveLocation,
selection,
booking,
setShowLocationModal,
isOpenDialog,
defaultValues,
setSelectedLocation,
} = props;
const { t } = useLocale();
const locationsQuery = trpc.viewer.locationOptions.useQuery();
useEffect(() => {
if (selection) {
locationFormMethods.setValue("locationType", selection?.value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selection]);
const locationFormSchema = z.object({
locationType: z.string(),
phone: z.string().optional(),
locationAddress: z.string().optional(),
locationLink: z
.string()
.optional()
.superRefine((val, ctx) => {
if (
eventLocationType &&
!eventLocationType.default &&
eventLocationType.linkType === "static" &&
eventLocationType.urlRegExp
) {
const valid = z
.string()
.regex(new RegExp(eventLocationType.urlRegExp || ""))
.safeParse(val).success;
if (!valid) {
const sampleUrl = eventLocationType.organizerInputPlaceholder;
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL for ${eventLocationType.label}. ${
sampleUrl ? "Sample URL: " + sampleUrl : ""
}`,
});
}
return;
}
const valid = z.string().url().optional().safeParse(val).success;
if (!valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid URL`,
});
}
return;
}),
displayLocationPublicly: z.boolean().optional(),
locationPhoneNumber: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional(),
});
const locationFormMethods = useForm({
mode: "onSubmit",
resolver: zodResolver(locationFormSchema),
});
const selectedLocation = useWatch({
control: locationFormMethods.control,
name: "locationType",
});
const eventLocationType = getEventLocationType(selectedLocation);
const defaultLocation = defaultValues?.find(
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
);
const LocationOptions = (() => {
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
if (!eventLocationType.variable) {
console.error("eventLocationType.variable can't be undefined");
return null;
}
return (
<div>
<div>
<Label htmlFor="locationInput">{t(eventLocationType.messageForOrganizer || "")}</Label>
<LocationInput
locationFormMethods={locationFormMethods}
eventLocationType={eventLocationType}
id="locationInput"
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
required
className="block w-full rounded-md border-gray-300 text-sm"
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
defaultValue={
(defaultLocation && defaultLocation[eventLocationType.defaultValueVariable]) || ""
}
/>
<ErrorMessage
errors={locationFormMethods.formState.errors}
name={eventLocationType.variable}
className="mt-1 text-sm text-red-500"
as="p"
/>
</div>
{!booking && (
<div className="mt-3">
<Controller
name="displayLocationPublicly"
control={locationFormMethods.control}
render={() => (
<CheckboxField
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) =>
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
}
informationIconText={t("display_location_info_badge")}
/>
)}
/>
</div>
)}
</div>
);
} else {
return <p className="text-sm">{getMessageForOrganizer(selectedLocation, t)}</p>;
}
})();
return (
<Dialog open={isOpenDialog}>
<DialogContent
type="creation"
Icon={Icon.FiMapPin}
useOwnActionButtons
title={t("edit_location")}
description={!booking ? t("this_input_will_shown_booking_this_event") : undefined}>
{booking && (
<>
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
<p className="mb-2 ml-1 text-sm text-black">
{getHumanReadableLocationValue(booking.location, t)}
</p>
</>
)}
<Form
form={locationFormMethods}
className="space-y-4"
handleSubmit={async (values) => {
const { locationType: newLocation, displayLocationPublicly } = values;
let details = {};
if (newLocation === LocationType.AttendeeInPerson) {
details = {
address: values.locationAddress,
};
}
if (newLocation === LocationType.InPerson) {
details = {
address: values.locationAddress,
};
}
const eventLocationType = getEventLocationType(newLocation);
// TODO: There can be a property that tells if it is to be saved in `link`
if (
newLocation === LocationType.Link ||
(!eventLocationType?.default && eventLocationType?.linkType === "static")
) {
details = { link: values.locationLink };
}
if (newLocation === LocationType.UserPhone) {
details = { hostPhoneNumber: values.locationPhoneNumber };
}
if (eventLocationType?.organizerInputType) {
details = {
...details,
displayLocationPublicly,
};
}
saveLocation(newLocation, details);
setShowLocationModal(false);
setSelectedLocation?.(undefined);
locationFormMethods.unregister([
"locationType",
"locationLink",
"locationAddress",
"locationPhoneNumber",
]);
}}>
<QueryCell
query={locationsQuery}
success={({ data: locationOptions }) => {
if (!locationOptions.length) return null;
if (booking) {
locationOptions.forEach((location) => {
if (location.label === "phone") {
location.options.filter((l) => l.value !== "phone");
}
});
}
return (
<Controller
name="locationType"
control={locationFormMethods.control}
render={() => (
<Select<{ label: string; value: string; icon?: string }>
maxMenuHeight={150}
name="location"
defaultValue={selection}
options={locationOptions}
components={{
Option: (props) => (
<components.Option {...props}>
<div className="flex items-center gap-3">
{props.data.icon && (
<img src={props.data.icon} alt="cover" className="h-3.5 w-3.5" />
)}
<span
className={classNames(
"text-sm font-medium",
props.isSelected ? "text-white" : "text-gray-900"
)}>
{props.data.label}
</span>
</div>
</components.Option>
),
}}
formatOptionLabel={(e) => (
<div className="flex items-center gap-2.5">
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
<span>{e.label}</span>
</div>
)}
formatGroupLabel={(e) => <p className="text-xs font-medium text-gray-600">{e.label}</p>}
isSearchable={false}
onChange={(val) => {
if (val) {
locationFormMethods.setValue("locationType", val.value);
locationFormMethods.unregister([
"locationLink",
"locationAddress",
"locationPhoneNumber",
]);
locationFormMethods.clearErrors([
"locationLink",
"locationPhoneNumber",
"locationAddress",
]);
setSelectedLocation?.(val);
}
}}
/>
)}
/>
);
}}
/>
{selectedLocation && LocationOptions}
<div className="mt-5">
<p className="text-sm text-gray-400">
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
Can&apos;t find the right video app? Visit our
<LinkText href="/apps/categories/video" classNameChildren="text-blue-400" target="_blank">
App Store
</LinkText>
.
</Trans>
</p>
</div>
<div className="mt-4 flex justify-end space-x-2">
<Button
onClick={() => {
setShowLocationModal(false);
setSelectedLocation?.(undefined);
locationFormMethods.unregister("locationType");
}}
type="button"
color="secondary">
{t("cancel")}
</Button>
<Button type="submit">{t("update")}</Button>
</div>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -1,13 +1,12 @@
import { EventTypeCustomInput } from "@prisma/client/";
import Link from "next/link";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { CAL_URL } from "@calcom/lib/constants";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
@@ -15,7 +14,9 @@ import {
Checkbox,
CustomInputItem,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
Icon,
Label,
SettingsToggle,
@@ -26,6 +27,8 @@ import {
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
import RequiresConfirmationController from "./RequiresConfirmationController";
const generateHashedLink = (id: number) => {
const translator = short();
const seed = `${id}:${new Date().getTime()}`;
@@ -41,11 +44,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
eventType.customInputs.sort((a, b) => a.id - b.id) || []
);
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
const [selectedCustomInput, setSelectedCustomInput] = useState<CustomInputParsed | undefined>(undefined);
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
@@ -99,7 +103,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<TextField
label={t("event_name")}
type="text"
placeholder={t("meeting_with_user")}
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
addOnSuffix={
@@ -123,15 +127,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
onCheckedChange={(e) => {
if (e && customInputs.length === 0) {
// Push a placeholders
setSelectedCustomInput(undefined);
setSelectedCustomInputModalOpen(true);
} else if (!e) {
setCustomInputs([]);
formMethods.setValue("customInputs", []);
}
}}>
<ul className="my-4">
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
<ul className="my-4 rounded-md border">
{customInputs.map((customInput, idx) => (
<CustomInputItem
key={idx}
question={customInput.label}
@@ -154,24 +156,17 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
setSelectedCustomInput(undefined);
setSelectedCustomInputModalOpen(true);
}}>
Add an input
{t("add_input")}
</Button>
)}
</SettingsToggle>
</div>
<hr />
<Controller
name="requiresConfirmation"
defaultValue={eventType.requiresConfirmation}
render={({ field: { value, onChange } }) => (
<SettingsToggle
title={t("requires_confirmation")}
description={t("requires_confirmation_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
disabled={seatsEnabled}
/>
)}
<RequiresConfirmationController
seatsEnabled={seatsEnabled}
metadata={eventType.metadata}
requiresConfirmation={requiresConfirmation}
onRequiresConfirmation={setRequiresConfirmation}
/>
<hr />
<Controller
@@ -254,7 +249,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<SettingsToggle
data-testid="hashedLinkCheck"
title={t("private_link")}
description={t("private_link_description")}
description={t("private_link_description", { appName: APP_NAME })}
checked={hashedLinkVisible}
onCheckedChange={(e) => {
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
@@ -307,6 +302,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
if (e) {
formMethods.setValue("disableGuests", true);
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("seatsPerTimeSlot", 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
@@ -352,15 +348,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<DialogContent
title={t("custom_event_name")}
description={t("custom_event_name_description")}
type="creation"
actionText="Create"
// Set event name back to what it was on close
actionOnClose={() => formMethods.setValue("eventName", eventType.eventName ?? "")}
actionOnClick={() => setShowEventNameTip(false)}>
type="creation">
<TextField
label={t("event_name")}
type="text"
placeholder={t("meeting_with_user")}
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
/>
@@ -370,6 +362,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p>
<p>{`{LOCATION} = ${t("event_location")}`}</p>
</div>
<DialogFooter>
<Button color="primary" onClick={() => setShowEventNameTip(false)}>
{t("create")}
</Button>
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")} />
</DialogFooter>
</DialogContent>
</Dialog>
)}
@@ -383,29 +381,40 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
type="creation"
Icon={Icon.FiPlus}
title={t("add_new_custom_input_field")}
useOwnActionButtons
description={t("this_input_will_shown_booking_this_event")}>
<CustomInputTypeForm
selectedCustomInput={selectedCustomInput}
onSubmit={(values) => {
const customInput: EventTypeCustomInput = {
const customInput: CustomInputParsed = {
id: -1,
eventTypeId: -1,
label: values.label,
placeholder: values.placeholder,
required: values.required,
type: values.type,
options: values.options,
};
if (selectedCustomInput) {
selectedCustomInput.label = customInput.label;
selectedCustomInput.placeholder = customInput.placeholder;
selectedCustomInput.required = customInput.required;
selectedCustomInput.type = customInput.type;
selectedCustomInput.options = customInput.options || undefined;
// Update by id
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
customInputs[inputIndex] = selectedCustomInput;
setCustomInputs(customInputs);
formMethods.setValue("customInputs", customInputs);
} else {
setCustomInputs(customInputs.concat(customInput));
formMethods.setValue("customInputs", customInputs.concat(customInput));
const concatted = customInputs.concat({
...customInput,
options: customInput.options,
});
console.log(concatted);
setCustomInputs(concatted);
formMethods.setValue("customInputs", concatted);
}
setSelectedCustomInputModalOpen(false);
}}
onCancel={() => {
+105 -13
View File
@@ -4,12 +4,13 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form";
import { MultiValue } from "react-select";
import { z } from "zod";
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Label, Select, Skeleton, TextField } from "@calcom/ui";
import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui";
import { slugify } from "@lib/slugify";
@@ -30,6 +31,22 @@ export const EventSetupTab = (
const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
value: mins,
label: t("multiple_duration_mins", { count: mins }),
}));
const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
MultiValue<{
value: number;
label: string;
}>
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
const [defaultDuration, setDefaultDuration] = useState(
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
);
const openLocationModal = (type: EventLocationType["type"]) => {
setSelectedLocation(locationOptions.find((option) => option.value === type));
@@ -113,6 +130,7 @@ export const EventSetupTab = (
{validLocations.length === 0 && (
<div className="flex">
<Select
placeholder={t("select")}
options={locationOptions}
isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
@@ -195,7 +213,7 @@ export const EventSetupTab = (
<div className="space-y-8">
<TextField
required
label={t("Title")}
label={t("title")}
defaultValue={eventType.title}
{...formMethods.register("title")}
/>
@@ -219,17 +237,91 @@ export const EventSetupTab = (
setValueAs: (v) => slugify(v),
})}
/>
<TextField
required
name="length"
type="number"
label={t("duration")}
addOnSuffix={<>{t("minutes")}</>}
defaultValue={eventType.length ?? 15}
onChange={(e) => {
formMethods.setValue("length", Number(e.target.value));
}}
/>
{multipleDuration ? (
<div className="space-y-4">
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("available_durations")}
</Skeleton>
<Select
isMulti
defaultValue={selectedMultipleDuration}
name="metadata.multipleDuration"
isSearchable={false}
className="h-auto !min-h-[36px] text-sm"
options={multipleDurationOptions}
onChange={(options) => {
const values = options
.map((opt) => opt.value)
.sort(function (a, b) {
return a - b;
});
setMultipleDuration(values);
setSelectedMultipleDuration(options);
if (!options.find((opt) => opt.value === defaultDuration?.value)) {
if (options.length > 0) {
setDefaultDuration(options[0]);
} else {
setDefaultDuration(null);
}
}
if (options.length === 1 && defaultDuration === null) {
setDefaultDuration(options[0]);
}
formMethods.setValue("metadata.multipleDuration", values);
}}
/>
</div>
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("default_duration")}
</Skeleton>
<Select
value={defaultDuration}
isSearchable={false}
name="length"
className="text-sm"
noOptionsMessage={() => t("default_duration_no_options")}
options={selectedMultipleDuration}
onChange={(option) => {
setDefaultDuration(
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
);
formMethods.setValue("length", Number(option?.value));
}}
/>
</div>
</div>
) : (
<TextField
required
name="length"
type="number"
label={t("duration")}
addOnSuffix={<>{t("minutes")}</>}
defaultValue={eventType.length ?? 15}
onChange={(e) => {
formMethods.setValue("length", Number(e.target.value));
}}
/>
)}
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
<SettingsToggle
title={t("allow_booker_to_select_duration")}
checked={multipleDuration !== undefined}
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
setMultipleDuration([]);
formMethods.setValue("metadata.multipleDuration", []);
formMethods.setValue("length", 0);
}
}}
/>
</div>
<div>
<Skeleton as={Label} loadingClassName="w-16">
{t("location")}
@@ -67,7 +67,7 @@ function getNavigation(props: {
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
icon: Icon.FiCalendar,
info: `Working Hours`, // TODO: Get this from props
info: `default_schedule_name`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
@@ -0,0 +1,156 @@
import * as RadioGroup from "@radix-ui/react-radio-group";
import { UnitTypeLongPlural } from "dayjs";
import { Trans } from "next-i18next";
import type { FormValues } from "pages/event-types/[type]";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Alert, Input, Label, SettingsToggle } from "@calcom/ui";
type RequiresConfirmationControllerProps = {
metadata: z.infer<typeof EventTypeMetaDataSchema>;
requiresConfirmation: boolean;
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
seatsEnabled: boolean;
};
export default function RequiresConfirmationController({
metadata,
requiresConfirmation,
onRequiresConfirmation,
seatsEnabled,
}: RequiresConfirmationControllerProps) {
const { t } = useLocale();
const [requiresConfirmationSetup, setRequiresConfirmationSetup] = useState(
metadata?.requiresConfirmationThreshold
);
const defaultRequiresConfirmationSetup = { time: 30, unit: "minutes" as UnitTypeLongPlural };
const formMethods = useFormContext<FormValues>();
useEffect(() => {
if (!requiresConfirmation) {
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
}
}, [requiresConfirmation]);
return (
<div className="block items-start sm:flex">
<div className={!seatsEnabled ? "w-full" : ""}>
{seatsEnabled ? (
<Alert severity="warning" title="Seats option doesn't support confirmation requirement" />
) : (
<Controller
name="requiresConfirmation"
control={formMethods.control}
render={() => (
<SettingsToggle
title={t("requires_confirmation")}
description={t("requires_confirmation_description")}
checked={requiresConfirmation}
onCheckedChange={(val) => {
formMethods.setValue("requiresConfirmation", val);
onRequiresConfirmation(val);
}}>
<RadioGroup.Root
defaultValue={
requiresConfirmation
? requiresConfirmationSetup === undefined
? "always"
: "notice"
: undefined
}
onValueChange={(val) => {
if (val === "always") {
formMethods.setValue("requiresConfirmation", true);
onRequiresConfirmation(true);
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
setRequiresConfirmationSetup(undefined);
} else if (val === "notice") {
formMethods.setValue("requiresConfirmation", true);
onRequiresConfirmation(true);
formMethods.setValue(
"metadata.requiresConfirmationThreshold",
requiresConfirmationSetup || defaultRequiresConfirmationSetup
);
}
}}>
<div className="-ml-2 flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
<div className="flex items-center">
<RadioGroup.Item
id="always"
value="always"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="always" className="!m-0 flex items-center">
{t("always_requires_confirmation")}
</Label>
</div>
<div className="flex items-center">
<RadioGroup.Item
id="notice"
value="notice"
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
</RadioGroup.Item>
<Label htmlFor="notice" className="!m-0 flex items-center">
<Trans
i18nKey="when_booked_with_less_than_notice"
defaults="When booked with less than <time></time> notice"
components={{
time: (
<div className="mx-2 flex">
<Input
type="number"
min={1}
onChange={(evt) => {
const val = Number(evt.target?.value);
setRequiresConfirmationSetup({
unit:
requiresConfirmationSetup?.unit ??
defaultRequiresConfirmationSetup.unit,
time: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
}}
className="!m-0 block w-16 rounded-md border-gray-300 text-sm [appearance:textfield]"
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
/>
<select
onChange={(evt) => {
const val = evt.target.value as UnitTypeLongPlural;
setRequiresConfirmationSetup({
time:
requiresConfirmationSetup?.time ??
defaultRequiresConfirmationSetup.time,
unit: val,
});
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
}}
className="ml-2 block h-9 rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
defaultValue={
metadata?.requiresConfirmationThreshold?.unit ||
defaultRequiresConfirmationSetup.unit
}>
<option value="minutes">{t("minute_timeUnit")}</option>
<option value="hours">{t("hour_timeUnit")}</option>
</select>
</div>
),
}}
/>
</Label>
</div>
</div>
</RadioGroup.Root>
</SettingsToggle>
)}
/>
)}
</div>
</div>
);
}
@@ -4,21 +4,20 @@ import { FormEvent, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
import { Avatar } from "@calcom/ui";
import { AvatarSSR } from "@components/ui/AvatarSSR";
interface IUserProfile {
user?: User;
}
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
type FormData = {
bio: string;
};
interface IUserProfileProps {
user: IOnboardingPageProps["user"];
}
const UserProfile = (props: IUserProfile) => {
const UserProfile = (props: IUserProfileProps) => {
const { user } = props;
const { t } = useLocale();
const avatarRef = useRef<HTMLInputElement>(null!);
@@ -100,7 +99,14 @@ const UserProfile = (props: IUserProfile) => {
return (
<form onSubmit={onSubmit}>
<div className="flex flex-row items-center justify-start rtl:justify-end">
{user && <AvatarSSR user={user} alt="Profile picture" className="h-16 w-16" />}
{user && (
<Avatar
alt={user.username || "user avatar"}
gravatarFallbackMd5={user.emailMd5}
size="lg"
imageSrc={imageSrc}
/>
)}
<input
ref={avatarRef}
type="hidden"
@@ -1,17 +1,18 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, TimezoneSelect } from "@calcom/ui";
import { UsernameAvailability } from "@components/ui/UsernameAvailability";
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
interface IUserSettingsProps {
user: User;
user: IOnboardingPageProps["user"];
nextStep: () => void;
}
@@ -23,11 +24,9 @@ const UserSettings = (props: IUserSettingsProps) => {
register,
handleSubmit,
formState: { errors },
control,
} = useForm({
defaultValues: {
name: user?.name || "",
username: user?.username || "",
},
reValidateMode: "onChange",
});
@@ -41,39 +40,19 @@ const UserSettings = (props: IUserSettingsProps) => {
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: onSuccess,
});
const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
const paymentRequired = stripeCustomer?.isPremium ? !stripeCustomer?.paidForPremium : false;
const onSubmit = handleSubmit((data) => {
if (paymentRequired) {
return;
}
mutation.mutate({
name: data.name,
timeZone: selectedTimeZone,
});
});
const [currentUsername, setCurrentUsername] = useState(user.username || "");
return (
<form onSubmit={onSubmit}>
<div className="space-y-6">
{/* Username textfield */}
<Controller
control={control}
name="username"
render={({ field: { value, ref, onChange } }) => (
<UsernameAvailability
readonly={true}
currentUsername={currentUsername}
setCurrentUsername={setCurrentUsername}
inputUsernameValue={value}
usernameRef={ref}
setInputUsernameValue={onChange}
user={user}
/>
)}
/>
<UsernameAvailabilityField user={user} />
{/* Full name textfield */}
<div className="w-full">
@@ -1,134 +0,0 @@
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
import React, { FC } from "react";
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
import { Button } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
import Select from "@components/ui/form/Select";
interface OptionTypeBase {
label: string;
value: EventTypeCustomInputType;
}
interface Props {
onSubmit: SubmitHandler<IFormInput>;
onCancel: () => void;
selectedCustomInput?: EventTypeCustomInput;
}
type IFormInput = EventTypeCustomInput;
const CustomInputTypeForm: FC<Props> = (props) => {
const { t } = useLocale();
const inputOptions: OptionTypeBase[] = [
{ value: EventTypeCustomInputType.TEXT, label: t("text") },
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
];
const { selectedCustomInput } = props;
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
const { register, control, handleSubmit } = useForm<IFormInput>({
defaultValues,
});
const selectedInputType = useWatch({ name: "type", control });
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value);
const onCancel = () => {
props.onCancel();
};
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<div className="mb-2">
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
{t("input_type")}
</label>
<Controller
name="type"
control={control}
render={({ field }) => (
<Select
id="type"
defaultValue={selectedInputOption}
options={inputOptions}
isSearchable={false}
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
onChange={(option) => option && field.onChange(option.value)}
value={selectedInputOption}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
</div>
<div className="mb-2">
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
{t("label")}
</label>
<div className="mt-1">
<input
type="text"
id="label"
required
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={selectedCustomInput?.label}
{...register("label", { required: true })}
/>
</div>
</div>
{(selectedInputType === EventTypeCustomInputType.TEXT ||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
<div className="mb-2">
<label htmlFor="placeholder" className="block text-sm font-medium text-gray-700">
{t("placeholder")}
</label>
<div className="mt-1">
<input
type="text"
id="placeholder"
className="block w-full rounded-sm border-gray-300 text-sm"
defaultValue={selectedCustomInput?.placeholder}
{...register("placeholder")}
/>
</div>
</div>
)}
<div className="flex h-5 items-center">
<input
id="required"
type="checkbox"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 ltr:mr-2 rtl:ml-2"
defaultChecked={selectedCustomInput?.required ?? true}
{...register("required")}
/>
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
{t("is_required")}
</label>
</div>
<input
type="hidden"
id="eventTypeId"
value={selectedCustomInput?.eventTypeId || -1}
{...register("eventTypeId", { valueAsNumber: true })}
/>
<input
type="hidden"
id="id"
value={selectedCustomInput?.id || -1}
{...register("id", { valueAsNumber: true })}
/>
<div className="mt-5 flex space-x-2 sm:mt-4">
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
{t("cancel")}
</Button>
<Button type="submit">{t("save")}</Button>
</div>
</form>
);
};
export default CustomInputTypeForm;
@@ -0,0 +1,73 @@
import type { Attendee, Booking, User } from "@prisma/client";
import type { FC } from "react";
import { useMemo } from "react";
import { JsonLd } from "react-schemaorg";
import type { EventReservation, Person, ReservationStatusType } from "schema-dts";
type EventSchemaUser = Pick<User, "name" | "email">;
type EventSchemaAttendee = Pick<Attendee, "name" | "email">;
interface EventReservationSchemaInterface {
reservationId: Booking["uid"];
eventName: Booking["title"];
startTime: Booking["startTime"];
endTime: Booking["endTime"];
organizer: EventSchemaUser | null;
attendees: EventSchemaAttendee[];
location: Booking["location"];
description: Booking["description"];
status: Booking["status"];
}
const EventReservationSchema: FC<EventReservationSchemaInterface> = ({
reservationId,
eventName,
startTime,
endTime,
organizer,
attendees,
location,
description,
status,
}) => {
const reservationStatus = useMemo<ReservationStatusType>(() => {
switch (status) {
case "ACCEPTED":
return "ReservationConfirmed";
case "REJECTED":
case "CANCELLED":
return "ReservationCancelled";
case "PENDING":
return "ReservationPending";
default:
return "ReservationHold";
}
}, [status]);
return (
<JsonLd<EventReservation>
item={{
"@context": "https://schema.org",
"@type": "EventReservation",
reservationId,
reservationStatus,
reservationFor: {
"@type": "Event",
name: eventName,
startDate: startTime.toString(),
endDate: endTime.toString(),
organizer: organizer
? ({ "@type": "Person", name: organizer.name, email: organizer.email } as Person)
: undefined,
attendee: attendees?.map(
(person) => ({ "@type": "Person", name: person.name, email: person.email } as Person)
),
location: location || undefined,
description: description || undefined,
},
}}
/>
);
};
export default EventReservationSchema;
+2 -1
View File
@@ -9,6 +9,7 @@ import {
MeetingImageProps,
} from "@calcom/lib/OgImages";
import { getBrowserInfo } from "@calcom/lib/browser/browser.utils";
import { APP_NAME } from "@calcom/lib/constants";
import { truncateOnWord } from "@calcom/lib/text";
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
@@ -79,7 +80,7 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
const image = getSeoImage("ogImage") + constructGenericImage({ title, description });
const truncatedDescription = truncateOnWord(description, 158);
const pageTitle = title + " | Cal.com";
const pageTitle = title + " | " + APP_NAME;
let seoObject = buildSeoMeta({
title: pageTitle,
image,
@@ -72,11 +72,7 @@ const DisableTwoFactorAuthModal = ({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
title={t("disable_2fa")}
description={t("disable_2fa_recommendation")}
type="creation"
useOwnActionButtons>
<DialogContent title={t("disable_2fa")} description={t("disable_2fa_recommendation")} type="creation">
<Form form={form} handleSubmit={handleDisable}>
<div className="mb-4">
<PasswordField
@@ -130,11 +130,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
title={t("enable_2fa")}
description={setupDescriptions[step]}
type="creation"
useOwnActionButtons>
<DialogContent title={t("enable_2fa")} description={setupDescriptions[step]} type="creation">
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}>
<div className="mb-4">
-207
View File
@@ -1,207 +0,0 @@
import React, { useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { objectKeys } from "@calcom/lib/objectKeys";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, Icon, showToast, TextField } from "@calcom/ui";
import ImageUploader from "@components/ImageUploader";
import SettingInputContainer from "@components/ui/SettingInputContainer";
interface Props {
team: TeamWithMembers | null | undefined;
}
export default function TeamSettings(props: Props) {
const { t } = useLocale();
const [hasErrors, setHasErrors] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const team = props.team;
const hasLogo = !!team?.logo;
const utils = trpc.useContext();
const mutation = trpc.viewer.teams.update.useMutation({
onError: (err) => {
setHasErrors(true);
setErrorMessage(err.message);
},
async onSuccess() {
await utils.viewer.teams.get.invalidate();
showToast(t("your_team_updated_successfully"), "success");
setHasErrors(false);
},
});
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const teamUrlRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const descriptionRef = useRef<HTMLTextAreaElement>() as React.MutableRefObject<HTMLTextAreaElement>;
const hideBrandingRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const logoRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
function updateTeamData() {
if (!team) return;
const variables = {
name: nameRef.current?.value,
slug: teamUrlRef.current?.value,
bio: descriptionRef.current?.value,
hideBranding: hideBrandingRef.current?.checked,
};
// remove unchanged variables
objectKeys(variables).forEach((key) => {
if (variables[key as keyof typeof variables] === team?.[key]) delete variables[key];
});
mutation.mutate({ id: team.id, ...variables });
}
function updateLogo(newLogo: string) {
if (!team) return;
logoRef.current.value = newLogo;
mutation.mutate({ id: team.id, logo: newLogo });
}
const removeLogo = () => updateLogo("");
return (
<div className="divide-y divide-gray-200 lg:col-span-9">
<div className="">
{hasErrors && <Alert severity="error" title={errorMessage} />}
<form
className="divide-y divide-gray-200 lg:col-span-9"
onSubmit={(e) => {
e.preventDefault();
updateTeamData();
}}>
<div className="py-6">
<div className="flex flex-col lg:flex-row">
<div className="flex-grow space-y-6">
<SettingInputContainer
Icon={Icon.FiLink}
label="Team URL"
htmlFor="team-url"
Input={
<TextField
name="" // typescript requires name but we don't want component to render name label
id="team-url"
addOnLeading={
<span className="inline-flex items-center rounded-l-sm border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500">
{process.env.NEXT_PUBLIC_WEBSITE_URL}/team/
</span>
}
ref={teamUrlRef}
defaultValue={team?.slug as string}
/>
}
/>
<SettingInputContainer
Icon={Icon.FiHash}
label="Team Name"
htmlFor="name"
Input={
<input
ref={nameRef}
type="text"
name="name"
id="name"
placeholder={t("your_team_name")}
required
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
defaultValue={team?.name as string}
/>
}
/>
<hr />
<div>
<SettingInputContainer
Icon={Icon.FiInfo}
label={t("about")}
htmlFor="about"
Input={
<>
<textarea
ref={descriptionRef}
id="about"
name="about"
rows={3}
defaultValue={team?.bio as string}
className="mt-1 block w-full rounded-sm border-gray-300 text-sm"
/>
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
</>
}
/>
</div>
<div>
<SettingInputContainer
Icon={Icon.FiImage}
label="Logo"
htmlFor="avatar"
Input={
<>
<div className="mt-1 flex">
<input
ref={logoRef}
type="hidden"
name="avatar"
id="avatar"
placeholder="URL"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
defaultValue={team?.logo ?? undefined}
/>
<ImageUploader
target="logo"
id="logo-upload"
buttonMsg={hasLogo ? t("edit_logo") : t("upload_a_logo")}
handleAvatarChange={updateLogo}
imageSrc={team?.logo ?? undefined}
/>
{hasLogo && (
<Button
onClick={removeLogo}
color="secondary"
type="button"
className="ml-1 py-1 text-xs">
{t("remove_logo")}
</Button>
)}
</div>
</>
}
/>
<hr className="mt-6" />
</div>
<div className="relative flex items-start">
<div className="flex h-5 items-center">
<input
id="hide-branding"
name="hide-branding"
type="checkbox"
ref={hideBrandingRef}
defaultChecked={team?.hideBranding}
className="h-4 w-4 rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500"
/>
</div>
<div className="text-sm ltr:ml-3 rtl:mr-3">
<label htmlFor="hide-branding" className="font-medium text-gray-700">
{t("disable_cal_branding")}
</label>
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
</div>
</div>
</div>
</div>
</div>
<div className="flex justify-end py-4">
<Button type="submit" color="primary">
{t("save")}
</Button>
</div>
</form>
</div>
</div>
);
}
@@ -1,126 +0,0 @@
import { MembershipRole } from "@prisma/client";
import Link from "next/link";
import { useRouter } from "next/router";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
import { ConfirmationDialogContent, Dialog, DialogTrigger, Icon, showToast } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
import LinkIconButton from "@components/ui/LinkIconButton";
export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers; role: MembershipRole }) {
const { t } = useLocale();
const utils = trpc.useContext();
const router = useRouter();
const permalink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/team/${props.team?.slug}`;
const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({
async onSuccess() {
await utils.viewer.teams.get.invalidate();
router.push(`/settings/teams`);
showToast(t("your_team_updated_successfully"), "success");
},
});
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
onSuccess: () => {
utils.viewer.teams.list.invalidate();
router.push(`/settings/teams`);
},
});
function deleteTeam() {
if (props.team?.id) deleteTeamMutation.mutate({ teamId: props.team.id });
}
function leaveTeam() {
if (props.team?.id)
acceptOrLeaveMutation.mutate({
teamId: props.team.id,
accept: false,
});
}
return (
<div className="space-y-6 px-2">
{(props.role === MembershipRole.OWNER || props.role === MembershipRole.ADMIN) && (
<CreateEventTypeButton
isIndividualTeam
canAddEvents={true}
options={[
{
teamId: props.team?.id,
name: props.team?.name,
slug: props.team?.slug,
image: props.team?.logo,
},
]}
/>
)}
<div className="space-y-1">
<Link href={permalink} passHref={true}>
<a target="_blank">
<LinkIconButton Icon={Icon.FiExternalLink}>{t("preview")}</LinkIconButton>
</a>
</Link>
<LinkIconButton
Icon={Icon.FiLink}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Copied to clipboard", "success");
}}>
{t("copy_link_team")}
</LinkIconButton>
{props.role === "OWNER" ? (
<Dialog>
<DialogTrigger asChild>
<LinkIconButton
onClick={(e) => {
e.stopPropagation();
}}
Icon={Icon.FiTrash}>
{t("disband_team")}
</LinkIconButton>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("disband_team")}
confirmBtnText={t("confirm_disband_team")}
onConfirm={deleteTeam}>
{t("disband_team_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
) : (
<Dialog>
<DialogTrigger asChild>
<LinkIconButton
Icon={Icon.FiLogOut}
onClick={(e) => {
e.stopPropagation();
}}>
{t("leave_team")}
</LinkIconButton>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title={t("leave_team")}
confirmBtnText={t("confirm_leave_team")}
onConfirm={leaveTeam}>
{t("leave_team_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
)}
</div>
{props.team?.id && props.role !== MembershipRole.MEMBER && (
<Link href={`/settings/teams/${props.team.id}/availability`}>
<div className="mt-5 hidden space-y-1 sm:block">
<LinkIconButton Icon={Icon.FiClock}>View Availability</LinkIconButton>
<p className="mt-2 text-sm text-gray-500">See your team members availability at a glance.</p>
</div>
</Link>
)}
</div>
);
}
+2 -2
View File
@@ -1,7 +1,7 @@
import classNames from "classnames";
import React from "react";
import { LOGO } from "@calcom/lib/constants";
import { APP_NAME, LOGO } from "@calcom/lib/constants";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";
@@ -21,7 +21,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
<HeadSeo title={props.title} description={props.description} />
{props.showLogo && (
// eslint-disable-next-line @next/next/no-img-element
<img className="mb-auto h-4" src={LOGO} alt="Cal.com Logo" />
<img className="mb-auto h-4" src={LOGO} alt={`${APP_NAME} Logo`} />
)}
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
@@ -3,7 +3,7 @@ import { debounce, noop } from "lodash";
import { useRouter } from "next/router";
import { RefCallback, useEffect, useMemo, useState } from "react";
import { getPremiumPlanMode, getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
import { fetchUsername } from "@calcom/lib/fetchUsername";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -24,9 +24,7 @@ import {
} from "@calcom/ui";
export enum UsernameChangeStatusEnum {
NORMAL = "NORMAL",
UPGRADE = "UPGRADE",
DOWNGRADE = "DOWNGRADE",
}
interface ICustomUsernameProps {
@@ -60,20 +58,14 @@ interface ICustomUsernameProps {
const obtainNewUsernameChangeCondition = ({
userIsPremium,
isNewUsernamePremium,
stripeCustomer,
}: {
userIsPremium: boolean;
isNewUsernamePremium: boolean;
stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
}) => {
if (!userIsPremium && isNewUsernamePremium && !stripeCustomer?.paidForPremium) {
if (!userIsPremium && isNewUsernamePremium) {
return UsernameChangeStatusEnum.UPGRADE;
}
if (userIsPremium && !isNewUsernamePremium && getPremiumPlanMode() === "subscription") {
return UsernameChangeStatusEnum.DOWNGRADE;
}
return UsernameChangeStatusEnum.NORMAL;
};
const PremiumTextfield = (props: ICustomUsernameProps) => {
@@ -106,7 +98,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
setIsInputUsernamePremium(data.premium);
setUsernameIsAvailable(data.available);
}, 150),
[]
[currentUsername]
);
useEffect(() => {
@@ -137,7 +129,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
});
// when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
const paymentRequired = !currentUsername && stripeCustomer?.isPremium && !stripeCustomer?.paidForPremium;
const paymentRequired = !currentUsername && stripeCustomer?.isPremium;
const usernameChangeCondition = obtainNewUsernameChangeCondition({
userIsPremium: isCurrentUsernamePremium,
@@ -194,7 +186,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
};
const saveUsername = () => {
if (usernameChangeCondition === UsernameChangeStatusEnum.NORMAL) {
if (usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE) {
updateUsername.mutate({
username: inputUsernameValue,
});
@@ -279,14 +271,6 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
{paymentMsg}
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
{usernameIsAvailable && (
<p className={classNames("mt-1 text-xs text-gray-900")}>
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE && (
<>{t("premium_to_standard_username_description")}</>
)}
</p>
)}
<Dialog open={openDialogSaveUsername}>
<DialogContent>
<div className="flex flex-row">
@@ -295,13 +279,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
</div>
<div className="mb-4 w-full px-4 pt-1">
<DialogHeader title={t("confirm_username_change_dialog_title")} />
{usernameChangeCondition && usernameChangeCondition !== UsernameChangeStatusEnum.NORMAL && (
<p className="-mt-4 mb-4 text-sm text-gray-800">
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE &&
t("change_username_standard_to_premium")}
{usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE &&
t("change_username_premium_to_standard")}
</p>
{usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
<p className="mb-4 text-sm text-gray-800">{t("change_username_standard_to_premium")}</p>
)}
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
@@ -323,8 +302,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<div className="mt-4 flex flex-row-reverse gap-x-2">
{/* redirect to checkout */}
{(usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE ||
usernameChangeCondition === UsernameChangeStatusEnum.DOWNGRADE) && (
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
<Button
type="button"
loading={updateUsername.isLoading}
@@ -336,7 +314,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
</Button>
)}
{/* Normal save */}
{usernameChangeCondition === UsernameChangeStatusEnum.NORMAL && (
{usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && (
<Button
type="button"
loading={updateUsername.isLoading}
@@ -347,10 +325,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
{t("save")}
</Button>
)}
<DialogClose asChild>
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</Button>
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</DialogClose>
</div>
</DialogContent>
@@ -124,6 +124,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
<Input
ref={usernameRef}
name="username"
value={inputUsernameValue}
autoComplete="none"
autoCapitalize="none"
autoCorrect="none"
@@ -133,7 +134,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
: ""
)}
defaultValue={currentUsername}
onChange={(event) => {
event.preventDefault();
setInputUsernameValue(event.target.value);
@@ -194,10 +194,8 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
{t("save")}
</Button>
<DialogClose asChild>
<Button color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</Button>
<DialogClose color="secondary" onClick={() => setOpenDialogSaveUsername(false)}>
{t("cancel")}
</DialogClose>
</div>
</DialogContent>
@@ -1,6 +1,67 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
import { User } from "@calcom/prisma/client";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
import { PremiumTextfield } from "./PremiumTextfield";
import { UsernameTextfield } from "./UsernameTextfield";
export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : PremiumTextfield;
interface UsernameAvailabilityFieldProps {
onSuccessMutation?: () => void;
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
user: Pick<
User,
| "username"
| "name"
| "email"
| "bio"
| "avatar"
| "timeZone"
| "weekStart"
| "hideBranding"
| "theme"
| "plan"
| "brandColor"
| "darkBrandColor"
| "timeFormat"
| "metadata"
>;
}
export const UsernameAvailabilityField = ({
onSuccessMutation,
onErrorMutation,
user,
}: UsernameAvailabilityFieldProps) => {
const [currentUsername, setCurrentUsername] = useState(user.username ?? "");
const formMethods = useForm({
defaultValues: {
username: currentUsername,
},
});
return (
<Controller
control={formMethods.control}
name="username"
render={({ field: { ref, onChange, value } }) => {
return (
<UsernameAvailability
currentUsername={currentUsername}
setCurrentUsername={setCurrentUsername}
inputUsernameValue={value}
usernameRef={ref}
setInputUsernameValue={onChange}
onSuccessMutation={onSuccessMutation}
onErrorMutation={onErrorMutation}
user={user}
/>
);
}}
/>
);
};
+2 -2
View File
@@ -1,6 +1,6 @@
import { DefaultSeoProps, NextSeoProps } from "next-seo";
import { SEO_IMG_DEFAULT, SEO_IMG_OGIMG } from "@calcom/lib/constants";
import { APP_NAME, SEO_IMG_DEFAULT, SEO_IMG_OGIMG } from "@calcom/lib/constants";
export type HeadSeoProps = {
title: string;
@@ -27,7 +27,7 @@ export const seoConfig: {
defaultNextSeo: DefaultSeoProps;
} = {
headSeo: {
siteName: "Cal.com",
siteName: APP_NAME,
},
defaultNextSeo: {
twitter: {
+1 -1
View File
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
export default function usePublicPage() {
const router = useRouter();
const isPublicPage = ["/[user]", "/success", "/cancel", "/reschedule"].find((route) =>
const isPublicPage = ["/[user]", "/booking", "/cancel", "/reschedule"].find((route) =>
router.pathname.startsWith(route)
);
return isPublicPage;
+18
View File
@@ -0,0 +1,18 @@
import { useRouter } from "next/router";
export default function useRouterQuery<T extends string>(name: T) {
const router = useRouter();
const existingQueryParams = router.asPath.split("?")[1];
const urlParams = new URLSearchParams(existingQueryParams);
const query = Object.fromEntries(urlParams);
const setQuery = (newValue: string | number | null | undefined) => {
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
};
return { [name]: query[name], setQuery } as {
[K in T]: string | undefined;
} & { setQuery: typeof setQuery };
}
+2 -4
View File
@@ -1,5 +1,3 @@
import { User } from "@prisma/client";
export function isBrandingHidden<TUser extends Pick<User, "hideBranding" | "plan">>(user: TUser) {
return user.hideBranding && user.plan !== "FREE";
export function isBrandingHidden(hideBrandingSetting: boolean, belongsToActiveTeam: boolean) {
return belongsToActiveTeam && hideBrandingSetting;
}
+23
View File
@@ -7,6 +7,29 @@ export type EmbedProps = {
export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
const ssrResponse = await getServerSideProps(context);
const embed = context.query.embed;
if ("redirect" in ssrResponse) {
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
const destinationUrlObj = new URL(ssrResponse.redirect.destination, "https://base");
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
const newDestinationUrl =
destinationUrlObj.pathname +
"/embed?" +
destinationUrlObj.searchParams.toString() +
"&embed=" +
embed;
return {
...ssrResponse,
redirect: {
...ssrResponse.redirect,
destination: newDestinationUrl,
},
};
}
if (!("props" in ssrResponse)) {
return ssrResponse;
}
+15
View File
@@ -149,6 +149,21 @@ const nextConfig = {
source: "/router",
destination: "/apps/routing-forms/router",
},
{
source: "/success/:path*",
has: [
{
type: "query",
key: "uid",
value: "(?<uid>.*)",
},
],
destination: "/booking/:uid/:path*",
},
{
source: "/cancel/:path*",
destination: "/booking/:path*",
},
/* TODO: have these files being served from another deployment or CDN {
source: "/embed/embed.js",
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.3.1",
"version": "2.3.6",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -38,7 +38,7 @@
"@calcom/tsconfig": "*",
"@calcom/ui": "*",
"@daily-co/daily-js": "^0.26.0",
"@formkit/auto-animate": "^1.0.0-beta.1",
"@formkit/auto-animate": "^1.0.0-beta.5",
"@glidejs/glide": "^3.5.2",
"@heroicons/react": "^1.0.6",
"@hookform/error-message": "^2.0.0",
@@ -107,12 +107,14 @@
"react-live-chat-loader": "^2.7.3",
"react-multi-email": "^0.5.3",
"react-phone-number-input": "^3.2.7",
"react-schemaorg": "^2.0.0",
"react-select": "^5.4.0",
"react-timezone-select": "^1.3.2",
"react-use-intercom": "1.5.1",
"react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.7",
"rrule": "^2.7.1",
"schema-dts": "^1.1.0",
"short-uuid": "^4.2.0",
"stripe": "^9.16.0",
"superjson": "1.9.1",
+9 -5
View File
@@ -1,9 +1,9 @@
import { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { Icon } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
@@ -38,7 +38,7 @@ export default function Custom404() {
setUrl(`${WEBSITE_URL}/signup?username=${username.replace("/", "")}`);
}, [username]);
const isSuccessPage = router.asPath.startsWith("/success");
const isSuccessPage = router.asPath.startsWith("/booking");
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
const isSignup = router.asPath.startsWith("/signup");
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
@@ -64,7 +64,7 @@ export default function Custom404() {
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
{t("signup_requires")}
</h1>
<p className="mt-4">{t("signup_requires_description")}</p>
<p className="mt-4">{t("signup_requires_description", { companyName: COMPANY_NAME })}</p>
</div>
<div className="mt-12">
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
@@ -191,7 +191,11 @@ export default function Custom404() {
</span>
) : isCalcom ? (
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
{t("the_username")} <strong className="text-blue-500">cal.com{username}</strong>{" "}
{t("the_username")}{" "}
<strong className="text-blue-500">
{new URL(WEBSITE_URL).hostname}
{username}
</strong>{" "}
{t("is_still_available")} <span className="text-blue-500">{t("register_now")}</span>.
</a>
) : (
+6 -3
View File
@@ -1,13 +1,16 @@
import Head from "next/head";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
export default function Error500() {
const { t } = useLocale();
return (
<div className="flex h-screen">
<Head>
<title>Something unexpected occurred | Cal.com</title>
<title>Something unexpected occurred | {APP_NAME}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="m-auto text-center">
@@ -24,7 +27,7 @@ export default function Error500() {
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
away for you.
</p>
<Button href={`${WEBSITE_URL}/support`}>Contact support</Button>
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
<Button color="secondary" href="javascript:history.back()" className="ml-2">
Go back
</Button>
+18 -3
View File
@@ -10,8 +10,9 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import prisma from "@calcom/prisma";
import { User } from "@calcom/prisma/client";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr";
@@ -110,6 +111,11 @@ async function getUserPageProps(context: GetStaticPropsContext) {
},
],
},
teams: {
include: {
team: true,
},
},
},
});
@@ -121,7 +127,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
"strikethrough",
]);
if (!user || !user.eventTypes) return { notFound: true };
if (!user || !user.eventTypes.length) return { notFound: true };
const [eventType]: (typeof user.eventTypes[number] & {
users: Pick<User, "name" | "username" | "hideBranding" | "plan" | "timeZone">[];
@@ -150,6 +156,13 @@ async function getUserPageProps(context: GetStaticPropsContext) {
locations: privacyFilteredLocations(locations),
descriptionAsSafeHTML: eventType.description ? md.render(eventType.description) : null,
});
// Check if the user you are logging into has any active teams
const hasActiveTeam =
user.teams.filter((m) => {
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return false;
return true;
}).length > 0;
return {
props: {
@@ -167,6 +180,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
away: user?.away,
isDynamic: false,
trpcState: ssg.dehydrate(),
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam),
},
revalidate: 10, // seconds
};
@@ -224,7 +238,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, {
metadata: (eventType.metadata || {}) as JSONObject,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations),
users: users.map((user) => {
@@ -262,6 +276,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
isDynamic: true,
away: false,
trpcState: ssg.dehydrate(),
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
},
revalidate: 10, // seconds
};
+3 -1
View File
@@ -12,6 +12,7 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { bookEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import getBooking, { GetBookingType } from "@lib/getBooking";
@@ -113,7 +114,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = {
...eventTypeRaw,
metadata: (eventTypeRaw.metadata || {}) as JSONObject,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
@@ -126,6 +127,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
schedulingType: null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
users: users.map((u) => ({
id: u.id,
name: u.name,
+3 -4
View File
@@ -3,10 +3,9 @@ import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps
type Props = Record<string, unknown> & DocumentProps;
function toRunBeforeReactOnClient() {
const calEmbedMode =
location.search.includes("embed=") ||
/* Iframe Name */
window.name.includes("cal-embed");
const calEmbedMode = typeof new URL(document.URL).searchParams.get("embed") === "string";
/* Iframe Name */
window.name.includes("cal-embed");
window.isEmbed = () => {
// Once an embed mode always an embed mode
+22 -4
View File
@@ -13,13 +13,14 @@ import path from "path";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, verifyPassword, isPasswordValid } from "@calcom/lib/auth";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
import rateLimit from "@calcom/lib/rateLimit";
import { serverConfig } from "@calcom/lib/serverConfig";
import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
import { randomString } from "@lib/random";
@@ -63,6 +64,11 @@ const providers: Provider[] = [
password: true,
twoFactorEnabled: true,
twoFactorSecret: true,
teams: {
include: {
team: true,
},
},
},
});
@@ -116,6 +122,13 @@ const providers: Provider[] = [
intervalInMs: 60 * 1000, // 1 minute
});
await limiter.check(10, user.email); // 10 requests per minute
// Check if the user you are logging into has any active teams
const hasActiveTeams =
user.teams.filter((m) => {
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return false;
return true;
}).length > 0;
// authentication success- but does it meet the minimum password requirements?
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
@@ -125,6 +138,7 @@ const providers: Provider[] = [
email: user.email,
name: user.name,
role: "USER",
belongsToActiveTeam: hasActiveTeams,
};
}
@@ -134,6 +148,7 @@ const providers: Provider[] = [
email: user.email,
name: user.name,
role: user.role,
belongsToActiveTeam: hasActiveTeams,
};
},
}),
@@ -204,9 +219,9 @@ if (true) {
});
const emailTemplate = Handlebars.compile(emailFile);
transporter.sendMail({
from: `${process.env.EMAIL_FROM}` || "Cal.com",
from: `${process.env.EMAIL_FROM}` || APP_NAME,
to: identifier,
subject: "Your sign-in link for Cal.com",
subject: "Your sign-in link for " + APP_NAME,
html: emailTemplate({
base_url: WEBAPP_URL,
signin_url: url,
@@ -272,6 +287,7 @@ export default NextAuth({
email: user.email,
role: user.role,
impersonatedByUID: user?.impersonatedByUID,
belongsToActiveTeam: user?.belongsToActiveTeam,
};
}
@@ -307,6 +323,7 @@ export default NextAuth({
email: existingUser.email,
role: existingUser.role,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
};
}
@@ -324,6 +341,7 @@ export default NextAuth({
username: token.username as string,
role: token.role as UserPermissionRole,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
},
};
return calendsoSession;
-68
View File
@@ -1,68 +0,0 @@
import { MembershipRole } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import slugify from "@lib/slugify";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session?.user?.id) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const ownerUser = await prisma.user.findFirst({
where: {
id: session.user.id,
},
select: {
id: true,
username: true,
createdDate: true,
name: true,
plan: true,
email: true,
},
});
if (req.method === "POST") {
const slug = slugify(req.body.name);
const nameCollisions = await prisma.team.count({
where: {
OR: [{ name: req.body.name }, { slug: slug }],
},
});
if (nameCollisions > 0) {
return res.status(409).json({ errorCode: "TeamNameCollision", message: "Team name already taken." });
}
const createTeam = await prisma.team.create({
data: {
name: req.body.name,
slug: slug,
},
});
await prisma.membership.create({
data: {
teamId: createTeam.id,
userId: session.user.id,
role: MembershipRole.OWNER,
accepted: true,
},
});
// Sync Services: Close.com
closeComUpsertTeamUser(createTeam, ownerUser, MembershipRole.OWNER);
return res.status(201).json({ message: "Team created" });
}
res.status(404).json({ message: "Team not found" });
}
+39 -5
View File
@@ -1,21 +1,55 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { InferGetStaticPropsType, NextPageContext } from "next";
import { ChangeEventHandler, useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { classNames } from "@calcom/lib";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client";
import { AllApps, AppsLayout, AppStoreCategories, TrendingAppsSlider } from "@calcom/ui";
import { AllApps, AppsLayout, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
import { ssgInit } from "@server/lib/ssg";
function AppsSearch({
onChange,
className,
}: {
onChange: ChangeEventHandler<HTMLInputElement>;
className?: string;
}) {
return (
<TextField
className="!border-gray-100 bg-gray-100 !pl-0 focus:!ring-offset-0"
addOnLeading={<Icon.FiSearch className="h-4 w-4 text-gray-500" />}
addOnClassname="!border-gray-100"
containerClassName={classNames("focus:!ring-offset-0", className)}
type="search"
autoComplete="false"
onChange={onChange}
/>
);
}
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
const { t } = useLocale();
const [searchText, setSearchText] = useState<string | undefined>(undefined);
return (
<AppsLayout isPublic heading={t("app_store")} subtitle={t("app_store_description")}>
<AppStoreCategories categories={categories} />
<TrendingAppsSlider items={appStore} />
<AllApps apps={appStore} />
<AppsLayout
isPublic
heading={t("app_store")}
subtitle={t("app_store_description")}
actions={(className) => (
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
)}>
{!searchText && (
<>
<AppStoreCategories categories={categories} />
<TrendingAppsSlider items={appStore} />
</>
)}
<AllApps apps={appStore} searchText={searchText} />
</AppsLayout>
);
}
+1 -1
View File
@@ -4,6 +4,7 @@ import z from "zod";
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
import { InstallAppButton } from "@calcom/app-store/components";
import { InstalledAppVariants } from "@calcom/app-store/utils";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { App } from "@calcom/types/App";
@@ -11,7 +12,6 @@ import { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProp
import {
Alert,
Button,
DisconnectIntegration,
EmptyScreen,
Icon,
InstalledAppsLayout,
+2 -2
View File
@@ -3,7 +3,7 @@ import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { getPremiumPlanPrice } from "@calcom/app-store/stripepayment/lib/utils";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import stripe from "@calcom/features/ee/payments/server/stripe";
import {
hostedCal,
@@ -161,7 +161,7 @@ const getStripePremiumUsernameUrl = async ({
customer: customer.id,
line_items: [
{
price: getPremiumPlanPrice(),
price: getPremiumMonthlyPlanPriceId(),
quantity: 1,
},
],
+6 -2
View File
@@ -29,6 +29,8 @@ import { HttpError } from "@lib/core/http/error";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import EditableHeading from "@components/ui/EditableHeading";
import { ssgInit } from "@server/lib/ssg";
const querySchema = z.object({
schedule: stringOrNumber,
});
@@ -91,7 +93,7 @@ export default function Availability({ schedule }: { schedule: number }) {
return (
<Shell
backPath="/availability"
title={data?.schedule.name && data.schedule.name + " | " + t("availability")}
title={data?.schedule.name ? data.schedule.name + " | " + t("availability") : t("availability")}
heading={
<Controller
control={form.control}
@@ -209,14 +211,16 @@ export default function Availability({ schedule }: { schedule: number }) {
);
}
export const getStaticProps: GetStaticProps = (ctx) => {
export const getStaticProps: GetStaticProps = async (ctx) => {
const params = querySchema.safeParse(ctx.params);
const ssg = await ssgInit(ctx);
if (!params.success) return { notFound: true };
return {
props: {
schedule: params.data.schedule,
trpcState: ssg.dehydrate(),
},
revalidate: 10, // seconds
};
+32
View File
@@ -1,4 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { GetServerSidePropsContext } from "next";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -10,6 +11,8 @@ import { HttpError } from "@lib/core/http/error";
import SkeletonLoader from "@components/availability/SkeletonLoader";
import { ssrInit } from "@server/lib/ssr";
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
const { t } = useLocale();
const utils = trpc.useContext();
@@ -45,6 +48,24 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
},
});
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async ({ schedule }) => {
await utils.viewer.availability.list.invalidate();
showToast(
t("availability_updated_successfully", {
scheduleName: schedule.name,
}),
"success"
);
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
}
},
});
// Adds smooth delete button - item fades and old item slides into place
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
@@ -71,6 +92,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
}}
key={schedule.id}
schedule={schedule}
updateDefault={updateMutation.mutate}
deleteFunction={deleteMutation.mutate}
/>
))}
@@ -93,3 +115,13 @@ export default function AvailabilityPage() {
</div>
);
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -23,6 +23,7 @@ import {
} from "@calcom/embed-core/embed-iframe";
import { parseRecurringEvent } from "@calcom/lib";
import CustomBranding from "@calcom/lib/CustomBranding";
import { APP_NAME } from "@calcom/lib/constants";
import { formatTime } from "@calcom/lib/date-fns";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -33,13 +34,14 @@ import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/t
import { localStorage } from "@calcom/lib/webstorage";
import prisma, { baseUserSelect } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, Icon } from "@calcom/ui";
import { timeZone } from "@lib/clock";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking";
import EventReservationSchema from "@components/schemas/EventReservationSchema";
import { HeadSeo } from "@components/seo/head-seo";
import { ssrInit } from "@server/lib/ssr";
@@ -147,6 +149,7 @@ const querySchema = z.object({
uid: z.string(),
allRemainingBookings: stringToBoolean,
cancel: stringToBoolean,
changes: stringToBoolean,
reschedule: stringToBoolean,
isSuccessBookingPage: z.string().optional(),
});
@@ -159,26 +162,33 @@ export default function Success(props: SuccessProps) {
allRemainingBookings,
isSuccessBookingPage,
cancel: isCancellationMode,
changes,
} = querySchema.parse(router.query);
if (isCancellationMode && typeof window !== "undefined") {
if ((isCancellationMode || changes) && typeof window !== "undefined") {
window.scrollTo(0, document.body.scrollHeight);
}
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location)
? props.bookingInfo.location[0] || ""
: props.bookingInfo.location || "";
? props.bookingInfo.location[0]
: // If there is no location set then we default to Cal Video
"integrations:daily";
if (!location) {
// Can't use logger.error because it throws error on client. stdout isn't available to it.
console.error(`No location found `);
}
const name = props.bookingInfo?.user?.name;
const email = props.bookingInfo?.user?.email;
const status = props.bookingInfo?.status;
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
const cancellationReason = props.bookingInfo.cancellationReason;
const attendeeName =
typeof props?.bookingInfo?.attendees?.[0]?.name === "string"
? props?.bookingInfo?.attendees?.[0]?.name
: "Nameless";
const [is24h, setIs24h] = useState(isBrowserLocale24h());
const { data: session } = useSession();
@@ -189,6 +199,7 @@ export default function Success(props: SuccessProps) {
const isEmbed = useIsEmbed();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const [calculatedDuration, setCalculatedDuration] = useState<number | undefined>(undefined);
function setIsCancellationMode(value: boolean) {
if (value) router.query.cancel = "true";
@@ -199,8 +210,6 @@ export default function Success(props: SuccessProps) {
});
}
const attendeeName = typeof name === "string" ? name : "Nameless";
const eventNameObject = {
attendeeName,
eventType: props.eventType.title,
@@ -220,7 +229,7 @@ export default function Success(props: SuccessProps) {
useEffect(() => {
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/success"));
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/booking"));
}
}, [telemetry]);
@@ -231,7 +240,7 @@ export default function Success(props: SuccessProps) {
sdkActionManager.fire("bookingSuccessful", {
eventType,
date: date.toString(),
duration: eventType.length,
duration: calculatedDuration,
organizer: {
name: users[0].name || "Nameless",
email: users[0].email || "Email-less",
@@ -245,6 +254,12 @@ export default function Success(props: SuccessProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventType, needsConfirmation]);
useEffect(() => {
setCalculatedDuration(
dayjs(props.bookingInfo.endTime).diff(dayjs(props.bookingInfo.startTime), "minutes")
);
}, []);
function eventLink(): string {
const optional: { location?: string } = {};
if (location) {
@@ -263,7 +278,9 @@ export default function Success(props: SuccessProps) {
title: eventName,
description: props.eventType.description ? props.eventType.description : undefined,
/** formatted to required type of description ^ */
duration: { minutes: props.eventType.length },
duration: {
minutes: calculatedDuration,
},
...optional,
});
@@ -305,6 +322,19 @@ export default function Success(props: SuccessProps) {
return (
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
{!isEmbed && (
<EventReservationSchema
reservationId={bookingInfo.uid}
eventName={eventName}
startTime={bookingInfo.startTime}
endTime={bookingInfo.endTime}
organizer={bookingInfo.user}
attendees={bookingInfo.attendees}
location={locationToDisplay}
description={bookingInfo.description}
status={status}
/>
)}
{userIsOwner && !isEmbed && (
<div className="mt-2 ml-4 -mb-4">
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}>
@@ -361,7 +391,7 @@ export default function Success(props: SuccessProps) {
<div className="mt-6 mb-8 text-center last:mb-0">
<h3
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white"
data-testid="success-headline"
data-testid={isCancelled ? "cancelled-headline" : ""}
id="modal-headline">
{needsConfirmation && !isCancelled
? props.recurringBookings
@@ -389,6 +419,7 @@ export default function Success(props: SuccessProps) {
<div className="col-span-2 mb-6 last:mb-0">
<RecurringBookings
eventType={props.eventType}
duration={calculatedDuration}
recurringBookings={props.recurringBookings}
allRemainingBookings={allRemainingBookings}
date={date}
@@ -399,7 +430,7 @@ export default function Success(props: SuccessProps) {
{(bookingInfo?.user || bookingInfo?.attendees) && (
<>
<div className="font-medium">{t("who")}</div>
<div className="col-span-2 mb-6 last:mb-0">
<div className="col-span-2 last:mb-0">
<>
{bookingInfo?.user && (
<div className="mb-3">
@@ -441,13 +472,40 @@ export default function Success(props: SuccessProps) {
)}
{customInputs &&
Object.keys(customInputs).map((key) => {
// This breaks if you have two label that are the same.
// TODO: Fix this in another PR
const customInput = customInputs[key as keyof typeof customInputs];
const eventTypeCustomFound = eventType.customInputs?.find((ci) => ci.label === key);
return (
<>
{customInput !== "" && (
{eventTypeCustomFound?.type === "RADIO" && (
<>
<div className="mt-2 pr-3 font-medium">{key}</div>
<div className="col-span-2 mt-2 mb-2">
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
{eventTypeCustomFound.label}
</div>
<div className="col-span-3 mt-1 mb-2">
{eventTypeCustomFound.options &&
eventTypeCustomFound.options.map((option) => {
const selected = option.label == customInput;
return (
<div
key={option.label}
className={classNames(
"flex space-x-1",
!selected && "text-gray-500"
)}>
<p>{option.label}</p>
<span>{option.label === customInput && "✅"}</span>
</div>
);
})}
</div>
</>
)}
{eventTypeCustomFound?.type !== "RADIO" && customInput !== "" && (
<>
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">{key}</div>
<div className="col-span-3 mt-2 mb-2">
{typeof customInput === "boolean" ? (
<p>{customInput ? "true" : "false"}</p>
) : (
@@ -469,7 +527,7 @@ export default function Success(props: SuccessProps) {
)}
</div>
</div>
{!needsConfirmation &&
{(!needsConfirmation || !userIsOwner) &&
!isCancelled &&
(!isCancellationMode ? (
<>
@@ -513,119 +571,125 @@ export default function Success(props: SuccessProps) {
/>
</>
))}
{userIsOwner && !needsConfirmation && !isCancellationMode && !isCancelled && (
<>
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
<div className="text-bookingdark align-center flex flex-row justify-center pt-8">
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
{t("add_to_calendar")}
</span>
<div className="justify-left mt-1 flex text-left sm:mt-0">
<Link
href={
`https://calendar.google.com/calendar/r/eventedit?dates=${date
.utc()
.format("YYYYMMDDTHHmmss[Z]")}/${date
.add(props.eventType.length, "minute")
.utc()
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
props.eventType.description
}` +
(typeof location === "string"
? "&location=" + encodeURIComponent(location)
: "") +
(props.eventType.recurringEvent
? "&recur=" +
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
: "")
}>
<a className="mr-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white">
<svg
className="-mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Google</title>
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
</svg>
</a>
</Link>
<Link
href={
encodeURI(
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
props.eventType.description +
"&enddt=" +
date.add(props.eventType.length, "minute").utc().format() +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Microsoft Outlook</title>
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
</svg>
</a>
</Link>
<Link
href={
encodeURI(
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
props.eventType.description +
"&enddt=" +
date.add(props.eventType.length, "minute").utc().format() +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Microsoft Office</title>
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
</svg>
</a>
</Link>
<Link href={"data:text/calendar," + eventLink()}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
download={props.eventType.title + ".ics"}>
<svg
version="1.1"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000"
className="mr-1 -mt-1.5 inline-block h-4 w-4">
<title>{t("other")}</title>
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
</svg>
</a>
</Link>
{userIsOwner &&
!needsConfirmation &&
!isCancellationMode &&
!isCancelled &&
calculatedDuration && (
<>
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
<div className="text-bookingdark align-center flex flex-row justify-center pt-8">
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
{t("add_to_calendar")}
</span>
<div className="justify-left mt-1 flex text-left sm:mt-0">
<Link
href={
`https://calendar.google.com/calendar/r/eventedit?dates=${date
.utc()
.format("YYYYMMDDTHHmmss[Z]")}/${date
.add(calculatedDuration, "minute")
.utc()
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
props.eventType.description
}` +
(typeof location === "string"
? "&location=" + encodeURIComponent(location)
: "") +
(props.eventType.recurringEvent
? "&recur=" +
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
: "")
}>
<a className="mr-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white">
<svg
className="-mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Google</title>
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
</svg>
</a>
</Link>
<Link
href={
encodeURI(
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
props.eventType.description +
"&enddt=" +
date.add(calculatedDuration, "minute").utc().format() +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Microsoft Outlook</title>
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
</svg>
</a>
</Link>
<Link
href={
encodeURI(
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
props.eventType.description +
"&enddt=" +
date.add(calculatedDuration, "minute").utc().format() +
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
date.utc().format() +
"&subject=" +
eventName
) + (location ? "&location=" + location : "")
}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
target="_blank">
<svg
className="mr-1 -mt-1.5 inline-block h-4 w-4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<title>Microsoft Office</title>
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
</svg>
</a>
</Link>
<Link href={"data:text/calendar," + eventLink()}>
<a
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
download={props.eventType.title + ".ics"}>
<svg
version="1.1"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000"
className="mr-1 -mt-1.5 inline-block h-4 w-4">
<title>{t("other")}</title>
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
</svg>
</a>
</Link>
</div>
</div>
</div>
</>
)}
</>
)}
{session === null && !(userIsOwner || props.hideBranding) && (
<>
<hr className="border-bookinglightest dark:border-darkgray-300 mt-8" />
<div className="border-bookinglightest text-booking-lighter dark:border-darkgray-300 pt-8 text-center text-xs dark:text-white">
<a href="https://cal.com/signup">{t("create_booking_link_with_calcom")}</a>
<a href="https://cal.com/signup">
{t("create_booking_link_with_calcom", { appName: APP_NAME })}
</a>
<form
onSubmit={(e) => {
@@ -669,6 +733,7 @@ type RecurringBookingsProps = {
eventType: SuccessProps["eventType"];
recurringBookings: SuccessProps["recurringBookings"];
date: dayjs.Dayjs;
duration: number | undefined;
is24h: boolean;
allRemainingBookings: boolean;
isCancelled: boolean;
@@ -677,6 +742,7 @@ type RecurringBookingsProps = {
export function RecurringBookings({
eventType,
recurringBookings,
duration,
date,
allRemainingBookings,
is24h,
@@ -689,6 +755,8 @@ export function RecurringBookings({
? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1))
: null;
if (!duration) return null;
if (recurringBookingsSorted && allRemainingBookings) {
return (
<>
@@ -707,7 +775,7 @@ export function RecurringBookings({
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
<br />
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(dateStr).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "}
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
<span className="text-bookinglight">({timeZone()})</span>
</div>
))}
@@ -725,7 +793,7 @@ export function RecurringBookings({
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
<br />
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(dateStr).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "}
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
<span className="text-bookinglight">({timeZone()})</span>
</div>
))}
@@ -741,7 +809,7 @@ export function RecurringBookings({
{dayjs.tz(date, timeZone()).format("MMMM DD, YYYY")}
<br />
{formatTime(date, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(date).add(eventType.length, "m"), is24h ? 24 : 12, timeZone())}{" "}
{formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
<span className="text-bookinglight">({timeZone()})</span>
</div>
);
@@ -762,6 +830,7 @@ const getEventTypesFromDB = async (id: number) => {
requiresConfirmation: true,
userId: true,
successRedirectUrl: true,
customInputs: true,
locations: true,
price: true,
currency: true,
@@ -828,22 +897,25 @@ const handleSeatsEventTypeOnBooking = (
seatsShowAttendees: boolean | null;
[x: string | number | symbol]: unknown;
},
booking: Partial<
bookingInfo: Partial<
Prisma.BookingGetPayload<{ include: { attendees: { select: { name: true; email: true } } } }>
>,
email: string
) => {
if (eventType?.seatsPerTimeSlot !== null) {
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
delete booking.description;
delete bookingInfo.description;
} else {
return;
}
if (!eventType.seatsShowAttendees) {
const attendee = booking?.attendees?.find((a) => a.email === email);
booking["attendees"] = attendee ? [attendee] : [];
const attendee = bookingInfo?.attendees?.find((a) => {
return a.email === email;
});
bookingInfo["attendees"] = attendee ? [attendee] : [];
}
return;
return bookingInfo;
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
@@ -865,6 +937,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
smsReminderNumber: true,
recurringEventId: true,
startTime: true,
endTime: true,
location: true,
status: true,
cancellationReason: true,
@@ -901,6 +974,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
const eventTypeRaw = !bookingInfo.eventTypeId
? getDefaultEvent(eventTypeSlug || "")
@@ -920,7 +994,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
select: baseUserSelect,
});
if (user) {
eventTypeRaw.users.push(user);
eventTypeRaw.users.push({
...user,
avatar: "",
allowDynamicBooking: true,
});
}
}
@@ -936,6 +1014,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
const profile = {
@@ -947,7 +1026,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
slug: eventType.team?.slug || eventType.users[0]?.username || null,
};
if (bookingInfo !== null && email) {
if (bookingInfo !== null && email && eventType.seatsPerTimeSlot) {
handleSeatsEventTypeOnBooking(eventType, bookingInfo, email);
}
+77 -34
View File
@@ -14,9 +14,18 @@ import { useInViewObserver } from "@lib/hooks/useInViewObserver";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
import { ssgInit } from "@server/lib/ssg";
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
type RecurringInfo = {
recurringEventId: string | null;
count: number;
firstDate: Date | null;
bookings: { [key: string]: Date[] };
};
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
const descriptionByStatus: Record<BookingListingStatus, string> = {
@@ -73,6 +82,18 @@ export default function Bookings() {
return true;
};
let recurringInfoToday: RecurringInfo | undefined;
const bookingsToday =
query.data?.pages.map((page) =>
page.bookings.filter((booking: BookingOutput) => {
recurringInfoToday = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return new Date(booking.startTime).toDateString() === new Date().toDateString();
})
)[0] || [];
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
return (
@@ -83,45 +104,65 @@ export default function Bookings() {
)}
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && !isEmpty && (
<div className="pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
{/* TODO: add today only for the current day
<p className="pb-3 text-xs font-medium leading-4 text-gray-500 uppercase">{t("today")}</p>
*/}
<div className="overflow-hidden rounded-md border border-gray-200">
<table className="w-full max-w-full table-fixed">
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<>
{!!bookingsToday.length && status === "upcoming" && (
<div className="mb-6 pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("today")}</p>
<div className="overflow-hidden rounded-md border border-gray-200">
<table className="w-full max-w-full table-fixed">
<tbody className="divide-y divide-gray-200 bg-white">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfo}
recurringInfo={recurringInfoToday}
{...booking}
/>
);
})}
</Fragment>
))}
</tbody>
</table>
))}
</Fragment>
</tbody>
</table>
</div>
</div>
)}
<div className="pt-2 xl:pt-0">
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("all")}</p>
<div className="overflow-hidden rounded-md border border-gray-200">
<table className="w-full max-w-full table-fixed">
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</tbody>
</table>
</div>
<div className="p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
<div className="p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:mx-6 xl:pt-0">
@@ -140,14 +181,16 @@ export default function Bookings() {
);
}
export const getStaticProps: GetStaticProps = (ctx) => {
export const getStaticProps: GetStaticProps = async (ctx) => {
const params = querySchema.safeParse(ctx.params);
const ssg = await ssgInit(ctx);
if (!params.success) return { notFound: true };
return {
props: {
status: params.data.status,
trpcState: ssg.dehydrate(),
},
};
};
-24
View File
@@ -1,24 +0,0 @@
import { GetServerSidePropsContext } from "next";
import z from "zod";
const querySchema = z.object({
uid: z.string(),
allRemainingBookings: z
.string()
.optional()
.transform((val) => (val ? JSON.parse(val) : false)),
});
export default function Type() {
return <></>;
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { allRemainingBookings, uid } = querySchema.parse(context.query);
return {
redirect: {
permanent: false,
destination: `/success?uid=${uid}&allRemainingBookings=${allRemainingBookings}&cancel=true`,
},
};
};
+1
View File
@@ -166,6 +166,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
previousPage: context.req.headers.referer ?? null,
booking,
users: [user.username],
isBrandingHidden: user.hideBranding,
},
};
};
+3 -1
View File
@@ -4,6 +4,7 @@ import { JSONObject } from "superjson/dist/types";
import { parseRecurringEvent } from "@calcom/lib";
import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -69,7 +70,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!eventTypeRaw) return { notFound: true };
const eventType = {
...eventTypeRaw,
metadata: (eventTypeRaw.metadata || {}) as JSONObject,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
@@ -78,6 +79,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
...e,
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
schedulingType: null,
users: users.map((u) => ({
id: u.id,
+41 -16
View File
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
@@ -16,15 +16,15 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
import { Form, showToast } from "@calcom/ui";
import { asStringOrThrow } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
@@ -39,6 +39,7 @@ import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayo
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
import { getTranslation } from "@server/lib/i18n";
import { ssrInit } from "@server/lib/ssr";
export type FormValues = {
title: string;
@@ -63,7 +64,7 @@ export type FormValues = {
displayLocationPublicly?: boolean;
phone?: string;
}[];
customInputs: EventTypeCustomInput[];
customInputs: CustomInputParsed[];
users: string[];
schedule: number;
periodType: PeriodType;
@@ -87,6 +88,8 @@ export type FormValues = {
bookingLimits?: BookingLimit;
};
export type CustomInputParsed = typeof customInputSchema._output;
const querySchema = z.object({
tabName: z
.enum(["setup", "availability", "apps", "limits", "recurring", "team", "advanced", "workflows"])
@@ -150,6 +153,21 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
endDate: new Date(eventType.periodEndDate || Date.now()),
});
const metadata = eventType.metadata;
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
if (!!team) {
metadata.config = {
...metadata.config,
useHostSchedulesForTeamEvent:
typeof eventType.metadata.config?.useHostSchedulesForTeamEvent !== "undefined"
? eventType.metadata.config?.useHostSchedulesForTeamEvent === true
: !!eventType.schedule,
};
} else {
// Make sure non-team events NEVER have this config key;
delete metadata.config?.useHostSchedulesForTeamEvent;
}
const formMethods = useForm<FormValues>({
defaultValues: {
title: eventType.title,
@@ -172,17 +190,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
findDurationType(eventType.minimumBookingNotice),
eventType.minimumBookingNotice
),
// fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
metadata: {
...eventType.metadata,
config: {
...eventType.metadata.config,
useHostSchedulesForTeamEvent:
typeof eventType.metadata.config?.useHostSchedulesForTeamEvent !== "undefined"
? eventType.metadata.config?.useHostSchedulesForTeamEvent === true
: !!eventType.schedule,
},
},
metadata,
},
});
@@ -256,6 +264,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
recurringEvent,
locations,
metadata,
customInputs,
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled,
@@ -266,7 +275,17 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
if (bookingLimits) {
const isValid = validateBookingLimitOrder(bookingLimits);
if (!isValid) throw new Error("Booking limits must be in accending order. [day,week,month,year]");
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
if (metadata?.multipleDuration !== undefined) {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!metadata?.multipleDuration?.includes(input.length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
}
updateMutation.mutate({
@@ -283,6 +302,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
seatsPerTimeSlot,
seatsShowAttendees,
metadata,
customInputs,
});
}}>
<div ref={animationParentRef} className="space-y-6">
@@ -297,6 +317,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const { req, query } = context;
const session = await getSession({ req });
const typeParam = parseInt(asStringOrThrow(query.type));
const ssr = await ssrInit(context);
if (Number.isNaN(typeParam)) {
return {
@@ -480,6 +501,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
// const parsedMetaData = _EventTypeModel.parse(newMetadata);
const parsedMetaData = newMetadata;
const parsedCustomInputs = (rawEventType.customInputs || []).map((input) => customInputSchema.parse(input));
const eventType = {
...restEventType,
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
@@ -487,6 +510,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
locations: locations as unknown as LocationObject[],
metadata: parsedMetaData,
customInputs: parsedCustomInputs,
};
// backwards compat
@@ -531,6 +555,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
team: eventTypeObject.team || null,
teamMembers,
currentUserMembership,
trpcState: ssr.dehydrate(),
},
};
};
+19 -10
View File
@@ -1,10 +1,11 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import Head from "next/head";
import { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import CreateEventTypeButton from "@calcom/features/eventtypes/components/CreateEventTypeButton";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
import {
@@ -12,7 +13,6 @@ import {
Button,
ButtonGroup,
ConfirmationDialogContent,
CreateEventTypeButton,
Dialog,
Dropdown,
DropdownItem,
@@ -38,6 +38,8 @@ import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup";
import { ssrInit } from "@server/lib/ssr";
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
@@ -440,7 +442,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
navigator
.share({
title: t("share"),
text: t("share_event"),
text: t("share_event", { appName: APP_NAME }),
url: calLink,
})
.then(() => showToast(t("link_shared"), "success"))
@@ -531,9 +533,9 @@ const EventTypeListHeading = ({
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
<a className="font-bold">{profile?.name || ""}</a>
</Link>
{membershipCount && (
{membershipCount && teamId && (
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2">
<Link href="/settings/teams">
<Link href={`/settings/teams/${teamId}/members`}>
<a>
<Badge variant="gray">
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
@@ -579,12 +581,9 @@ const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
const EventTypesPage = () => {
const { t } = useLocale();
return (
<div>
<Head>
<title>Home | Cal.com</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell
heading={t("event_types_page_title") as string}
subtitle={t("event_types_page_subtitle") as string}
@@ -622,4 +621,14 @@ const EventTypesPage = () => {
);
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default EventTypesPage;
@@ -5,20 +5,20 @@ import { useRouter } from "next/router";
import { z } from "zod";
import { getSession } from "@calcom/lib/auth";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client";
import { Button, StepCard, Steps } from "@calcom/ui";
import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
import UserProfile from "@components/getting-started/steps-views/UserProfile";
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
interface IOnboardingPageProps {
user: User;
}
export type IOnboardingPageProps = inferSSRProps<typeof getServerSideProps>;
const INITIAL_STEP = "user-settings";
const steps = ["user-settings", "connected-calendar", "setup-availability", "user-profile"] as const;
@@ -46,7 +46,7 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
const headers = [
{
title: `${t("welcome_to_cal_header")}`,
title: `${t("welcome_to_cal_header", { appName: APP_NAME })}`,
subtitle: [`${t("we_just_need_basic_info")}`, `${t("edit_form_later_subtitle")}`],
},
{
@@ -86,7 +86,9 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
data-testid="onboarding"
key={router.asPath}>
<Head>
<title>Cal.com - {t("getting_started")}</title>
<title>
{APP_NAME} - {t("getting_started")}
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
+4 -1
View File
@@ -42,6 +42,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!booking) {
return {
notFound: true,
} as {
notFound: true;
};
}
@@ -49,6 +51,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
// TODO: Show something in UI to let user know that this booking is not rescheduleable.
return {
notFound: true,
} as {
notFound: true;
};
}
@@ -62,7 +66,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
: booking.user?.username || "rick") /* This shouldn't happen */ +
"/" +
eventType?.slug;
return {
redirect: {
destination: "/" + eventPage + "?rescheduleUid=" + context.query.uid,
@@ -0,0 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[uid]";
export { default } from "../[uid]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+18 -2
View File
@@ -1,14 +1,30 @@
import { GetServerSidePropsContext } from "next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function AdminAppsView() {
const { t } = useLocale();
return (
<>
<Meta title="Apps" description="apps_description" />
<h1>App listing</h1>
<Meta title={t("apps")} description={t("apps_description")} />
<h1>{t("apps_listing")}</h1>
</>
);
}
AdminAppsView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AdminAppsView;
@@ -1,16 +1,19 @@
import { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react";
import { useRef } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, getAdminLayout as getLayout, Meta, TextField } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function AdminView() {
const { t } = useLocale();
const usernameRef = useRef<HTMLInputElement>(null);
return (
<>
<Meta title="Admin" description="Impersonation" />
<Meta title={t("admin")} description={t("impersonation")} />
<form
className="mb-6 w-full sm:w-1/2"
onSubmit={(e) => {
@@ -21,7 +24,7 @@ function AdminView() {
<div className="flex items-center space-x-2">
<TextField
containerClassName="w-full"
name="Impersonate User"
name={t("user_impersonation_heading")}
addOnLeading={<>{process.env.NEXT_PUBLIC_WEBSITE_URL}/</>}
ref={usernameRef}
hint={t("impersonate_user_tip")}
@@ -36,4 +39,14 @@ function AdminView() {
AdminView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AdminView;
+14
View File
@@ -1,5 +1,9 @@
import { GetServerSidePropsContext } from "next";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function AdminAppsView() {
return (
<>
@@ -11,4 +15,14 @@ function AdminAppsView() {
AdminAppsView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AdminAppsView;
+18 -2
View File
@@ -1,14 +1,30 @@
import { GetServerSidePropsContext } from "next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function AdminUsersView() {
const { t } = useLocale();
return (
<>
<Meta title="Users" description="users_description" />
<h1>Users listing</h1>
<Meta title={t("users")} description={t("users_description")} />
<h1>{t("users_listing")}</h1>
</>
);
}
AdminUsersView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AdminUsersView;
+14 -1
View File
@@ -1,3 +1,4 @@
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import { HelpScout, useChat } from "react-live-chat-loader";
@@ -8,6 +9,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, getSettingsLayout as getLayout, Icon, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
interface CtaRowProps {
title: string;
description: string;
@@ -64,7 +67,7 @@ const BillingView = () => {
<CtaRow title={t("billing_help_title")} description={t("billing_help_description")}>
<Button color="secondary" onClick={onContactSupportClick}>
{t("billing_help_cta")}
{t("contact_support")}
</Button>
</CtaRow>
{showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
@@ -75,4 +78,14 @@ const BillingView = () => {
BillingView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default BillingView;
+20 -3
View File
@@ -1,9 +1,11 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
import LicenseRequired from "@calcom/ee/common/components/v2/LicenseRequired";
import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/ApiKeyDialogForm";
import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListItem";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
@@ -17,6 +19,8 @@ import {
SkeletonLoader,
} from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const ApiKeysView = () => {
const { t } = useLocale();
@@ -43,7 +47,10 @@ const ApiKeysView = () => {
return (
<>
<Meta title="API Keys" description="API keys allow other apps to communicate with Cal.com" />
<Meta
title={t("api_keys")}
description={t("create_first_api_key_description", { appName: APP_NAME })}
/>
<LicenseRequired>
<>
@@ -70,7 +77,7 @@ const ApiKeysView = () => {
<EmptyScreen
Icon={Icon.FiLink}
headline={t("create_first_api_key")}
description={t("create_first_api_key_description")}
description={t("create_first_api_key_description", { appName: APP_NAME })}
buttonRaw={<NewApiKeyButton />}
/>
)}
@@ -79,7 +86,7 @@ const ApiKeysView = () => {
</LicenseRequired>
<Dialog open={apiKeyModal} onOpenChange={setApiKeyModal}>
<DialogContent type="creation" useOwnActionButtons>
<DialogContent type="creation">
<ApiKeyDialogForm handleClose={() => setApiKeyModal(false)} defaultValues={apiKeyToEdit} />
</DialogContent>
</Dialog>
@@ -89,4 +96,14 @@ const ApiKeysView = () => {
ApiKeysView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default ApiKeysView;
@@ -1,5 +1,8 @@
import { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react";
import { Controller, useForm } from "react-hook-form";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
@@ -16,9 +19,12 @@ import {
Switch,
} from "@calcom/ui";
const SkeletonLoader = () => {
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center">
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
@@ -40,7 +46,7 @@ const SkeletonLoader = () => {
const AppearanceView = () => {
const { t } = useLocale();
const session = useSession();
const utils = trpc.useContext();
const { data: user, isLoading } = trpc.viewer.me.useQuery();
@@ -67,7 +73,7 @@ const AppearanceView = () => {
},
});
if (isLoading) return <SkeletonLoader />;
if (isLoading) return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
if (!user) return null;
@@ -84,7 +90,7 @@ const AppearanceView = () => {
theme: values.theme || null,
});
}}>
<Meta title="Appearance" description="Manage settings for your booking appearance" />
<Meta title={t("appearance")} description={t("appearance_description")} />
<div className="mb-6 flex items-center text-sm">
<div>
<p className="font-semibold">{t("theme")}</p>
@@ -171,14 +177,15 @@ const AppearanceView = () => {
<div className="flex w-full text-sm">
<div className="mr-1 flex-grow">
<div className="flex items-center">
<p className="mr-2 font-semibold">{t("disable_cal_branding")}</p>
<p className="mr-2 font-semibold">{t("disable_cal_branding", { appName: APP_NAME })}</p>
<Badge variant="gray">{t("pro")}</Badge>
</div>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding")}</p>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p>
</div>
<div className="flex-none">
<Switch
id="hideBranding"
disabled={!session.data?.user.belongsToActiveTeam}
onCheckedChange={(checked) =>
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
}
@@ -203,6 +210,16 @@ const AppearanceView = () => {
AppearanceView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default AppearanceView;
interface ThemeLabelProps {
variant: "light" | "dark" | "system";
@@ -1,8 +1,10 @@
import { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { Fragment } from "react";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@@ -10,7 +12,6 @@ import {
Alert,
Badge,
Button,
DisconnectIntegration,
EmptyScreen,
getSettingsLayout as getLayout,
Icon,
@@ -22,12 +23,15 @@ import {
SkeletonButton,
SkeletonContainer,
SkeletonText,
showToast,
} from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
import { CalendarSwitch } from "@components/settings/CalendarSwitch";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = () => {
return (
<SkeletonContainer>
@@ -66,15 +70,17 @@ const CalendarsView = () => {
async onSettled() {
await utils.viewer.connectedCalendars.invalidate();
},
onSuccess: async () => {
showToast(t("calendar_updated_successfully"), "success");
},
onError: () => {
showToast(t("unexpected_error_try_again"), "error");
},
});
return (
<>
<Meta
title="Calendars"
description="Configure how your event types interact with your calendars"
CTA={<AddCalendarButton />}
/>
<Meta title={t("calendars")} description={t("calendars_description")} CTA={<AddCalendarButton />} />
<QueryCell
query={query}
customLoader={<SkeletonLoader />}
@@ -110,7 +116,7 @@ const CalendarsView = () => {
{t("check_for_conflicts")}
</h4>
<p className="pb-2 text-sm leading-5 text-gray-600">{t("select_calendars")}</p>
<List>
<List className="flex flex-col gap-6" noBorderTreatment>
{data.connectedCalendars.map((item) => (
<Fragment key={item.credentialId}>
{item.error && item.error.message && (
@@ -137,8 +143,8 @@ const CalendarsView = () => {
/>
)}
{item?.error === undefined && item.calendars && (
<ListItem expanded className="flex-col">
<div className="flex w-full flex-1 items-center space-x-3 pb-5 pl-1 pt-1 rtl:space-x-reverse">
<ListItem className="flex-col rounded-md">
<div className="flex w-full flex-1 items-center space-x-3 p-4 rtl:space-x-reverse">
{
// eslint-disable-next-line @next/next/no-img-element
item.integration.logo && (
@@ -164,7 +170,7 @@ const CalendarsView = () => {
<DisconnectIntegration
trashIcon
credentialId={item.credentialId}
buttonProps={{ size: "icon", color: "secondary" }}
buttonProps={{ className: "border border-gray-300" }}
/>
</div>
</div>
@@ -172,7 +178,7 @@ const CalendarsView = () => {
<p className="px-2 pt-4 text-sm text-neutral-500">
{t("toggle_calendars_conflict")}
</p>
<ul className="space-y-2 px-2 pt-4">
<ul className="space-y-2 p-4">
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
@@ -224,4 +230,14 @@ const CalendarsView = () => {
CalendarsView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default CalendarsView;
@@ -1,3 +1,4 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -5,7 +6,9 @@ import { trpc } from "@calcom/trpc/react";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
Dropdown,
DropdownMenuContent,
DropdownMenuItem,
@@ -22,9 +25,12 @@ import {
SkeletonText,
} from "@calcom/ui";
const SkeletonLoader = () => {
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
@@ -58,18 +64,19 @@ const ConferencingLayout = () => {
const [deleteAppModal, setDeleteAppModal] = useState(false);
const [deleteCredentialId, setDeleteCredentialId] = useState<number>(0);
if (isLoading) return <SkeletonLoader />;
if (isLoading)
return <SkeletonLoader title={t("conferencing")} description={t("conferencing_description")} />;
return (
<div className="w-full bg-white sm:mx-0 xl:mt-0">
<Meta title="Conferencing" description="Add your favourite video conferencing apps for your meetings" />
<Meta title={t("conferencing")} description={t("conferencing_description")} />
<List>
{apps?.items &&
apps.items
.map((app) => ({ ...app, title: app.title || app.name }))
.map((app) => (
<ListItem className="flex-col border-0" key={app.title}>
<div className="flex w-full flex-1 items-center space-x-3 pl-1 pt-1 rtl:space-x-reverse">
<div className="flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse">
{
// eslint-disable-next-line @next/next/no-img-element
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
@@ -111,10 +118,14 @@ const ConferencingLayout = () => {
title={t("Remove app")}
description={t("are_you_sure_you_want_to_remove_this_app")}
type="confirmation"
actionText={t("yes_remove_app")}
Icon={Icon.FiAlertCircle}
actionOnClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}
/>
Icon={Icon.FiAlertCircle}>
<DialogFooter>
<Button color="primary" onClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}>
{t("yes_remove_app")}
</Button>
<DialogClose />
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
@@ -122,4 +133,14 @@ const ConferencingLayout = () => {
ConferencingLayout.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default ConferencingLayout;
+18 -4
View File
@@ -1,3 +1,4 @@
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
@@ -21,9 +22,12 @@ import {
import { withQuery } from "@lib/QueryCell";
import { nameOfDay } from "@lib/core/i18n/weekday";
const SkeletonLoader = () => {
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
@@ -47,14 +51,14 @@ const GeneralQueryView = () => {
const { t } = useLocale();
const { data: user, isLoading } = trpc.viewer.me.useQuery();
if (isLoading) return <SkeletonLoader />;
if (isLoading) return <SkeletonLoader title={t("general")} description={t("general_description")} />;
if (!user) {
throw new Error(t("something_went_wrong"));
}
return (
<WithQuery
success={({ data }) => <GeneralView user={user} localeProp={data.locale} />}
customLoader={<SkeletonLoader />}
customLoader={<SkeletonLoader title={t("general")} description={t("general_description")} />}
/>
);
};
@@ -127,7 +131,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
weekStart: values.weekStart.value,
});
}}>
<Meta title="General" description="Manage settings for your language and timezone" />
<Meta title={t("general")} description={t("general_description")} />
<Controller
name="locale"
render={({ field: { value, onChange } }) => (
@@ -210,4 +214,14 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
GeneralQueryView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default GeneralQueryView;
+47 -33
View File
@@ -1,10 +1,12 @@
import { IdentityProvider } from "@prisma/client";
import crypto from "crypto";
import { GetServerSidePropsContext } from "next";
import { signOut } from "next-auth/react";
import { BaseSyntheticEvent, useEffect, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
@@ -14,7 +16,9 @@ import {
Avatar,
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogTrigger,
Form,
getSettingsLayout as getLayout,
@@ -32,11 +36,14 @@ import {
} from "@calcom/ui";
import TwoFactor from "@components/auth/TwoFactor";
import { UsernameAvailability } from "@components/ui/UsernameAvailability";
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
const SkeletonLoader = () => {
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center">
<SkeletonAvatar className=" h-12 w-12 px-4" />
@@ -205,11 +212,14 @@ const ProfileView = () => {
showToast(t("error_updating_settings"), "error");
};
if (isLoading || !user) return <SkeletonLoader />;
if (isLoading || !user)
return <SkeletonLoader title={t("profile")} description={t("profile_description")} />;
const isDisabled = isSubmitting || !isDirty;
return (
<>
<Meta title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
<Form
form={formMethods}
handleSubmit={(values) => {
@@ -219,7 +229,6 @@ const ProfileView = () => {
mutation.mutate(values);
}
}}>
<Meta title="Profile" description="Manage settings for your cal profile" />
<div className="flex items-center">
<Controller
control={formMethods.control}
@@ -233,7 +242,7 @@ const ProfileView = () => {
id="avatar-upload"
buttonMsg={t("change_avatar")}
handleAvatarChange={(newAvatar) => {
formMethods.setValue("avatar", newAvatar);
formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
}}
imageSrc={value}
/>
@@ -243,22 +252,10 @@ const ProfileView = () => {
/>
</div>
<div className="mt-8">
<Controller
control={formMethods.control}
name="username"
render={({ field: { ref, onChange, value } }) => {
return (
<UsernameAvailability
currentUsername={user?.username || ""}
inputUsernameValue={value}
usernameRef={ref}
setInputUsernameValue={onChange}
onSuccessMutation={onSuccessfulUsernameUpdate}
onErrorMutation={onErrorInUsernameUpdate}
user={user}
/>
);
}}
<UsernameAvailabilityField
user={user}
onSuccessMutation={onSuccessfulUsernameUpdate}
onErrorMutation={onErrorInUsernameUpdate}
/>
</div>
<div className="mt-8">
@@ -296,17 +293,11 @@ const ProfileView = () => {
</DialogTrigger>
<DialogContent
title={t("delete_account_modal_title")}
description={t("confirm_delete_account_modal")}
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
type="creation"
actionText={t("delete_my_account")}
actionProps={{
// @ts-expect-error data attributes aren't typed
"data-testid": "delete-account-confirm",
}}
Icon={Icon.FiAlertTriangle}
actionOnClick={(e) => e && onConfirmButton(e)}>
Icon={Icon.FiAlertTriangle}>
<>
<p className="mb-7">{t("delete_account_confirmation_message")}</p>
<p className="mb-7">{t("delete_account_confirmation_message", { appName: APP_NAME })}</p>
{isCALIdentityProviver && (
<PasswordField
data-testid="password"
@@ -326,6 +317,15 @@ const ProfileView = () => {
)}
{hasDeleteErrors && <Alert severity="error" title={deleteErrorMessage} />}
<DialogFooter>
<Button
color="primary"
data-testid="delete-account-confirm"
onClick={(e) => onConfirmButton(e)}>
{t("delete_my_account")}
</Button>
<DialogClose />
</DialogFooter>
</>
</DialogContent>
</Dialog>
@@ -337,9 +337,7 @@ const ProfileView = () => {
title={t("confirm_password")}
description={t("confirm_password_change_email")}
type="creation"
actionText={t("confirm")}
Icon={Icon.FiAlertTriangle}
actionOnClick={(e) => e && onConfirmPassword(e)}>
Icon={Icon.FiAlertTriangle}>
<>
<PasswordField
data-testid="password"
@@ -352,6 +350,12 @@ const ProfileView = () => {
/>
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
<DialogFooter>
<Button color="primary" onClick={(e) => onConfirmPassword(e)}>
{t("confirm")}
</Button>
<DialogClose />
</DialogFooter>
</>
</DialogContent>
</Dialog>
@@ -361,4 +365,14 @@ const ProfileView = () => {
ProfileView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default ProfileView;
@@ -1,3 +1,4 @@
import { GetServerSidePropsContext } from "next";
import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -13,6 +14,8 @@ import {
Switch,
} from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const ProfileImpersonationView = () => {
const { t } = useLocale();
const utils = trpc.useContext();
@@ -39,7 +42,7 @@ const ProfileImpersonationView = () => {
return (
<>
<Meta title="Impersonation Settings" description="" />
<Meta title={t("impersonation")} description={t("impersonation_description")} />
<Form
form={formMethods}
handleSubmit={({ disableImpersonation }) => {
@@ -74,4 +77,14 @@ const ProfileImpersonationView = () => {
ProfileImpersonationView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default ProfileImpersonationView;
@@ -1,4 +1,5 @@
import { IdentityProvider } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next";
import { useForm } from "react-hook-form";
@@ -7,6 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, Form, getSettingsLayout as getLayout, Meta, PasswordField, showToast } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
type ChangePasswordFormValues = {
oldPassword: string;
newPassword: string;
@@ -92,4 +95,14 @@ const PasswordView = () => {
PasswordView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default PasswordView;
+14
View File
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/sso/page/user-sso-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1,12 +1,37 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Badge, getSettingsLayout as getLayout, Loader, Meta, Switch } from "@calcom/ui";
import {
Badge,
getSettingsLayout as getLayout,
Meta,
Switch,
SkeletonButton,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center">
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
<SkeletonText className="h-8 w-full" />
</div>
</div>
</SkeletonContainer>
);
};
const TwoFactorAuthView = () => {
const utils = trpc.useContext();
@@ -16,11 +41,11 @@ const TwoFactorAuthView = () => {
const [enableModalOpen, setEnableModalOpen] = useState(false);
const [disableModalOpen, setDisableModalOpen] = useState(false);
if (isLoading) return <Loader />;
if (isLoading) return <SkeletonLoader title={t("2fa")} description={t("2fa_description")} />;
return (
<>
<Meta title="Two-Factor Authentication" description="Manage settings for your account passwords" />
<Meta title={t("2fa")} description={t("2fa_description")} />
<div className="mt-6 flex items-start space-x-4">
<Switch
checked={user?.twoFactorEnabled}
@@ -35,7 +60,7 @@ const TwoFactorAuthView = () => {
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="text-sm text-gray-600">Add an extra layer of security to your account.</p>
<p className="text-sm text-gray-600">{t("add_an_extra_layer_of_security")}</p>
</div>
</div>
@@ -68,4 +93,14 @@ const TwoFactorAuthView = () => {
TwoFactorAuthView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default TwoFactorAuthView;
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-appearance-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-billing-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-members-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-profile-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/sso/page/teams-sso-view";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
+17 -2
View File
@@ -1,14 +1,19 @@
import { GetServerSidePropsContext } from "next";
import Head from "next/head";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getWizardLayout as getLayout } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const CreateNewTeamPage = () => {
const { t } = useLocale();
return (
<>
<Head>
<title>Create a new Team</title>
<meta name="description" content="Create a new team to ease your organisational booking" />
<title>{t("create_new_team")}</title>
<meta name="description" content={t("create_new_team_description")} />
</Head>
<CreateANewTeamForm />
</>
@@ -17,4 +22,14 @@ const CreateNewTeamPage = () => {
CreateNewTeamPage.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default CreateNewTeamPage;
+2
View File
@@ -43,6 +43,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
name: true,
slug: true,
logo: true,
hideBranding: true,
eventTypes: {
where: {
slug: typeParam,
@@ -160,6 +161,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
previousPage: context.req.headers.referer ?? null,
booking,
trpcState: ssg.dehydrate(),
isBrandingHidden: team.hideBranding,
},
};
};
+3 -1
View File
@@ -4,6 +4,7 @@ import { JSONObject } from "superjson/dist/types";
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
import { parseRecurringEvent } from "@calcom/lib";
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import getBooking, { GetBookingType } from "@lib/getBooking";
@@ -92,9 +93,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventTypeObject = [eventType].map((e) => {
return {
...e,
metadata: (eventType.metadata || {}) as JSONObject,
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
users: eventType.users.map((u) => ({
id: u.id,
name: u.name,
+14
View File
@@ -1,8 +1,12 @@
import { GetServerSidePropsContext } from "next";
import { TeamsListing } from "@calcom/features/ee/teams/components";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Shell } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function Teams() {
const { t } = useLocale();
return (
@@ -22,4 +26,14 @@ function Teams() {
Teams.requiresLicense = false;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
export default Teams;
+14
View File
@@ -1 +1,15 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
export { default } from "@calcom/features/ee/workflows/pages/index";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};
+4 -10
View File
@@ -93,7 +93,7 @@ test.describe("pro user", () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({
url(url) {
return url.pathname === "/success";
return url.pathname.startsWith("/booking");
},
});
});
@@ -108,20 +108,14 @@ test.describe("pro user", () => {
await page.locator('[data-testid="cancel"]').first().click();
await page.waitForNavigation({
url: (url) => {
return url.pathname.startsWith("/success");
return url.pathname.startsWith("/booking");
},
});
// --- fill form
await page.locator('[data-testid="cancel"]').click();
await page.waitForNavigation({
url: (url) => {
return url.pathname.startsWith("/success");
},
});
const successHeadling = await page.locator('[data-testid="success-headline"]').innerText();
const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
await expect(successHeadling).toBe("This event is cancelled");
await expect(cancelledHeadline).toBe("This event is cancelled");
await page.goto(`/${pro.username}`);
await bookFirstEvent(page);
+5 -67
View File
@@ -1,8 +1,7 @@
import { expect } from "@playwright/test";
import { UserPlan } from "@prisma/client";
import { getFreePlanPrice, getProPlanPrice } from "@calcom/app-store/stripepayment/lib/utils";
import dayjs from "@calcom/dayjs";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import stripe from "@calcom/features/ee/payments/server/stripe";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -27,7 +26,7 @@ test.describe("Change username on settings", () => {
/** TODO: Find out why it's timing out */
test.fixme("User can change username", async ({ page, users, prisma }) => {
const user = await users.create({ plan: UserPlan.TRIAL });
const user = await users.create();
await user.login();
// Try to go homepage
@@ -50,28 +49,13 @@ test.describe("Change username on settings", () => {
expect(newUpdatedUser.username).toBe("demousernamex");
});
test("User trial can update to PREMIUM username", async ({ page, users }, testInfo) => {
test("User can update to PREMIUM username", async ({ page, users }, testInfo) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed");
test.skip(IS_SELF_HOSTED, "It shouldn't run on self hosted");
const user = await users.create({ plan: UserPlan.TRIAL });
const customer = await stripe.customers.create({ email: `${user?.username}@example.com` });
await stripe.subscriptionSchedules.create({
customer: customer.id,
start_date: "now",
end_behavior: "release",
phases: [
{
items: [{ price: getProPlanPrice() }],
trial_end: dayjs().add(14, "day").unix(),
end_date: dayjs().add(14, "day").unix(),
},
{
items: [{ price: getFreePlanPrice() }],
},
],
});
const user = await users.create();
await stripe.customers.create({ email: `${user?.username}@example.com` });
await user.login();
await page.goto("/settings/my-account/profile");
@@ -97,50 +81,4 @@ test.describe("Change username on settings", () => {
await expect(page).toHaveURL(/.*checkout.stripe.com/);
});
test("User PRO can update to PREMIUM username", async ({ page, users }, testInfo) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed");
test.skip(IS_SELF_HOSTED, "It shouldn't run on self hosted");
const user = await users.create({ plan: UserPlan.PRO });
const customer = await stripe.customers.create({ email: `${user?.username}@example.com` });
const paymentMethod = await stripe.paymentMethods.create({
type: "card",
card: {
number: "4242424242424242",
cvc: "123",
exp_month: 12,
exp_year: 2040,
},
});
await stripe.paymentMethods.attach(paymentMethod.id, { customer: customer.id });
await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: getProPlanPrice() }],
});
await user.login();
await page.goto("/settings/profile");
// Change username from normal to premium
const usernameInput = page.locator("[data-testid=username-input]");
await usernameInput.fill(`xx${testInfo.workerIndex}`);
// Click on save button
await page.click('button[type="submit"]');
// Validate modal text fields
const currentUsernameText = page.locator("[data-testid=current-username]").innerText();
const newUsernameText = page.locator("[data-testid=new-username]").innerText();
expect(currentUsernameText).not.toBe(newUsernameText);
// Click on Go to billing
await page.click("[data-testid=go-to-billing]", { timeout: 300 });
await page.waitForLoadState();
await expect(page).toHaveURL(/.*billing.stripe.com/);
});
});
@@ -37,7 +37,7 @@ test("dynamic booking", async ({ page, users }) => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({
url(url) {
return url.pathname === "/success";
return url.pathname.startsWith("/booking");
},
});
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
@@ -48,20 +48,14 @@ test("dynamic booking", async ({ page, users }) => {
await page.locator('[data-testid="cancel"]').first().click();
await page.waitForNavigation({
url: (url) => {
return url.pathname.startsWith("/success");
return url.pathname.startsWith("/booking");
},
});
// --- fill form
await page.locator('[data-testid="cancel"]').click();
await page.waitForNavigation({
url: (url) => {
return url.pathname.startsWith("/success");
},
});
const successHeadling = await page.locator('[data-testid="success-headline"]').innerText();
const cancelledHeadline = await page.locator('[data-testid="cancelled-headline"]').innerText();
await expect(successHeadling).toBe("This event is cancelled");
await expect(cancelledHeadline).toBe("This event is cancelled");
});
await users.deleteAll();
+1 -1
View File
@@ -112,7 +112,7 @@ async function bookEventOnThisPage(page: Page) {
// Make sure we're navigated to the success page
await page.waitForNavigation({
url(url) {
return url.pathname.endsWith("/success");
return url.pathname.startsWith("/booking");
},
});
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
+3 -3
View File
@@ -150,7 +150,7 @@ test.describe("Reschedule Tests", async () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*success/);
await expect(page).toHaveURL(/.*booking/);
await payment.delete();
});
@@ -168,7 +168,7 @@ test.describe("Reschedule Tests", async () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*success/);
await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
expect(newBooking).not.toBeNull();
@@ -189,7 +189,7 @@ test.describe("Reschedule Tests", async () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*success/);
await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
expect(newBooking).not.toBeNull();
+1 -1
View File
@@ -35,7 +35,7 @@ test.describe("Teams", () => {
await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail);
// Click [data-testid="invite-new-member-button"]
await page.locator('[data-testid="invite-new-member-button"]').click();
await expect(page.locator(`li:has-text("${inviteeEmail}PendingMemberNot on Cal.com")`)).toBeVisible();
await expect(page.locator(`li:has-text("${inviteeEmail}")`)).toBeVisible();
expect(await page.locator('[data-testid="pending-member-item"]').count()).toBe(2);
});
+2
View File
@@ -65,6 +65,8 @@ test("add webhook & test that creating an event triggers a webhook call", async
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
// if we change the shape of our webhooks, we can simply update this by clicking `u`
// console.log("BODY", body);
@@ -1 +1 @@
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}

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