Compare commits

..
111 Commits
Author SHA1 Message Date
Omar López 8143816c18 v2.5.0
release.yml / release (push) Canceled after 0s
2023-01-16 15:34:47 -07:00
Nafees Nazik 8243ac3c00 Fix: modal content hidden (#6508) 2023-01-16 18:08:34 +01:00
Alex van Andel 207e120d66 Bring back PublicShell (#6505) 2023-01-16 14:59:54 +01:00
Ronit PandaandPeer Richelsen b25d04da1c fix: tablet sidebar profile being too wide and not centered (#6490)
* feat: updated app list items styling issues inside settings/conferencing

* feat: fixed tablet sidebar width and not centered issue

* fix: removed un-related changes from this pr

* deleted .env

* fix: added the accidentaly deleted .env file

* fix: removed unwanted .env file from the pr

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-16 12:57:38 +00:00
Alex van Andel 7064acf756 Don't update the field when changing the select (#6486)
* Don't update the field when changing the select

* Fixing missing label

* Applied the correct y-8 vertical margin

* Changed the description vertical offset from title a bit more

* Add Trash icon on Availability list dropdown
2023-01-16 12:18:18 +00:00
Alex van Andel 716407ad90 Added fallback to hostsFixed too which is used in COLLECTIVE (#6500) 2023-01-16 10:46:06 +00:00
GitStart-Cal.comandgitstart-calcom af0b08bb2b [CAL-677] /event-type : Event Setup sidebar item doesn't reflect multiple durations (#6390)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-16 11:45:54 +01:00
Hariom Balhara 3e27735ccb Disable redirect for cancellation page (#6498) 2023-01-16 09:20:50 +00:00
Nafees Nazik 76c087f29c fix: wrong i18n placeholder in many locales (#6496)
* fix: czech translation

* fix: spanish translation

* fix: italian translation

* fix: japanese translation

* fix: korean translation

* fix: dutch translation

* fix: norwegian translation

* fix: polish translation

* fix: portuguese translation

* fix: portuguese translation

* fix: romanian translation

* fix: russian translation

* fix: serbian translation

* fix: swedish translation

* fix: turkish translation

* fix: ukrainian translation

* fix: vietnamese translation

* fix: chinese simplified translation

* fix: chinese traditional translation
2023-01-16 08:48:07 +00:00
Peer RichelsenandAlex van Andel e05bcf98c2 readding sidebar tips for everyone, added 3 more features to teams empty screen (#6475)
* readding sidebar tips for everyone

* added 3 more features to teams plan

* responsive fixes

* responsive fixes

* small responsive fix

* CTA alignment fixed

* Minor alignment fixes to Shell for event-types/[type]

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-15 18:54:52 +01:00
Udit TakkarandPeer Richelsen 636e88d467 fix: styling of dropdown component (#6440)
* fix: styling of dropdown component

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

* Apply suggestions from code review

* fix: use dropdown item in all pages

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

* chore: undo

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>
2023-01-15 11:40:40 +00:00
Nafees Nazik 34a61214d3 fix: app store spacing issues (#6481)
* feat: add equal spacing with gap

* fix: title style

* fix: title style

* fix: remove margin

* fix: remove margin

* fix: sub header spacing and slider button

* fix: category card text

* fix: app store spacing issues

* fix: tab in tablet

* fix: shell
2023-01-14 20:11:28 +01:00
Nafees Nazik b1af30e026 fix: german translation for reschedule booking email subject (#6483)
* fix: german translation for reschedule subject

* fix: add a full stop
2023-01-14 19:10:30 +01:00
Nafees Nazik 870b59d617 fix: darkmode in booking/[id] page (#6458)
* fix: text-area style

* fix: border styles
2023-01-14 16:49:15 +01:00
Peer Richelsen e25094ccab i18n udpate 2023-01-14 02:12:07 +01:00
Carina WollendorferandCarinaWolli 1376f0991f Reusable Upgrade component for teams only features (#6473)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-14 01:48:19 +01:00
443329b99f disables signups via env variable (#6212)
* disables signups via env variable

* Apply suggestions from code review

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-01-13 17:42:13 -07:00
Joe Au-Yeung a1fc4ec26d Disable email input if new user invited to team (#6471) 2023-01-13 17:21:57 -07:00
Udit TakkarandPeer Richelsen d462298e79 fix: remove website url in download recording (#6466)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-14 00:03:13 +00:00
Nafees NazikandPeer Richelsen 965cff38c2 fix: HOSTED_CAL_FEATURES env doesn't get passed to the frontend (#6392)
* fix: rename HOSTED_CAL_FEATURES into NEXT_PUBLIC_HOSTED_CAL_FEATURES

* chore: add the new env to example

* fix: docs link

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-13 23:51:37 +00:00
Joe Au-Yeungandkodiakhq[bot] f96e909dce Team invites - transform email or username to lower case (#6470)
* Fix string

* Use current hashPassword

* Transform email to lowercase

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-13 18:58:41 +00:00
Leo Giovanetti a4d6f7da62 Fixing GCal emailing notif and sequential deleting (#6460) 2023-01-13 11:29:42 -07:00
Nafees Nazikandkodiakhq[bot] ce9413e0dd fix: missing empty state in /apps/installed/web3 (#6377)
* refactor: turn enum into const assertion

* feat: add web3 to icon list

* fix: type

* fix: type cast query params and add category list

* fix: conditional rendering

* feat: add i18n keys

* fix: type errors

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-13 14:12:43 +00:00
Peer Richelsen 8fc0dfb763 booking page: only scroll to bottom on mobile (#6453)
* move useMediaQuery into packages

* revert logo.tsx
2023-01-13 17:40:02 +05:30
690edcf0a7 New Crowdin translations by Github Action (#6444)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-12 23:21:06 +00:00
Omar López b7dbf10068 v2.4.7
release.yml / release (push) Canceled after 0s
2023-01-12 16:10:07 -07:00
zomars 83b4ba94fc testing auto crowdin 2023-01-12 16:01:40 -07:00
62a09e7098 Small improvements for constants (#6409)
* use IS_SELF_HOSTED for CalComVersion instead of an additional check for hosted and self-hosted versions

* use IS_SELF_HOSTED for HOSTED_CAL_FEATURES constant to prevent an additional check

Co-authored-by: René Müller <rene.mueller@clicksports.de>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-12 22:25:25 +00:00
d2c4e2894d Check for session in addition for isPublic prop (#6443)
* Check for session in addition for isPublic prop

* Cleaner, more readable version

* Less code same result, no rerender

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-01-12 22:11:15 +00:00
Joe Au-YeungandOmar López 070d314897 Increase error logging details (#6442)
Co-authored-by: Omar López <zomars@me.com>
2023-01-12 21:43:40 +00:00
zomars 0a76368e98 Run required checks from Crowin PRs 2023-01-12 14:22:11 -07:00
99287d9eb4 Feature/fixed hosts (#6423)
* Save design updates for fixed round robin support

* wip - added Host relation

* DRY hostsFixed select

* Changes to allow isFixed in the Availability page

* Allow booking with fixed hosts

* Replace users with hosts if hosts is set

* Also prefer hosts over users here

* Prevent duplicates when hosts is saved

* Accidental slot duplication

* Attempt at making isFixed optional

* Sydney and Shiraz can live in harmony again

* No fixed hosts causes every to be true..

* Make hosts undefinable

* Small handleNewBooking fixes

* Similar fix to the hosts to check for empty-ness

* Default to isFixed false instead of true

* Fix event type list avatars

* Filter availableTimeSlots, collective ts's wont magically re-enable.

* (Further) Fixes to getAggregateWorkingHours

* Weird userId artifact that preceeds this branch, investigate later

* On user delete, remove host, on event type delete, also remove host

* Dynamic event types were incorrectly marked as isFixed=false

* Fixed notFound error when there are no users (but hosts)

* Fixes userIsOwner

* Oops, fixed isFixed users being included correctly

* Fixed Button styling on secondary darkmode

* Create exclusivity in selectable options

* fix: Location dropdown is overflowing #6376 (#6415)

* add `menuPlacement` react-select's props to `getReactSelectProps` to avoid dropdown overflow on small screen.
By default, set to "auto".

* CALCOM-6362 - [CAL-728] App Sidebar. Child items aren't sized/spaced properly (#6424)

* [CAL-728] App Sidebar. Child items aren't sized/spaced properly

* fix: undo logo size

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: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Fixing colors (#6429)

* Update website

* Update console

* Update yarn.lock

* Uses disable instead of filtering to be more clear

* Update EventTeamTab.tsx

* Merge conflict cleanup

* During test cases the dayjs() utcOffset is local time, this fixes that

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Thomas Brodusch <3238312+thomasbrodusch@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
2023-01-12 14:09:12 -07:00
zomars 100035cb89 Update yarn.lock 2023-01-12 11:37:19 -07:00
zomars 1750dbead6 Update console 2023-01-12 11:37:11 -07:00
zomars 4c8ed3d11f Update website 2023-01-12 11:36:54 -07:00
sean-brydon f21c4eb11e Fixing colors (#6429) 2023-01-12 16:57:43 +00:00
3f71a41d7e CALCOM-6362 - [CAL-728] App Sidebar. Child items aren't sized/spaced properly (#6424)
* [CAL-728] App Sidebar. Child items aren't sized/spaced properly

* fix: undo logo size

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: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-12 16:44:41 +00:00
Thomas Brodusch 1ba191c4ed fix: Location dropdown is overflowing #6376 (#6415)
* add `menuPlacement` react-select's props to `getReactSelectProps` to avoid dropdown overflow on small screen.
By default, set to "auto".
2023-01-12 09:29:37 -07:00
Nafees NazikandPeer Richelsen 6414903567 fix: guess correct timezone when onboarding (#6430)
* fix: guess correct timezone

* fix: redirect if on boarded

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-12 16:17:57 +00:00
e62d8b4596 fix: long words break UI (#6410)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-12 09:05:20 -07:00
Carina WollendorferandCarinaWolli e27b1b1bc2 Removes 'Test Action' button in workflows (#6418)
* comment out test action feature

* code clean up

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-12 09:03:30 -07:00
zomars 9507138f29 Labeler fixes 2023-01-12 08:52:21 -07:00
GitStart-Cal.comandgitstart-calcom e5fa202a09 CALCOM-6398 - [CAL-749] remove ethSignature from query if unused (#6425)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-12 14:55:00 +01:00
Udit TakkarandAlex van Andel 447098bc97 fix: use i18n properly in utils (#6363)
* fix: use i18n properly in utils

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

* chore

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: Alex van Andel <me@alexvanandel.com>
2023-01-12 05:22:55 +00:00
Udit Takkar dc5e042cdd fix: remove flex-nowrap (#6416)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-12 01:24:39 +00:00
Leo Giovanetti 92fb365b54 Fix for Request Email Reminder (#6421)
* Fix

* Selecting id
2023-01-11 22:12:11 -03:00
161f5c7000 Adds "Upgrade" paywall to Workflow SMS features instead of disabling them (#6412)
* adds upgrade badge to dropdown

* add space between batch and check

* add upgrade tool tip

* fix import

* code clean up

* fix type error

* fix type error

* fix type errors

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-11 18:41:45 +00:00
c8744768c4 Refactored buildSlots (#6389)
* Refactored buildSlots

* Added typing for computedAvailableSlots

* Adds very simple boundary logic to make sure slots start at the right times

* Logic was cutting off too early

* Woops, should be eventLength, not frequency

* Expect 12:15 slots as availability is till 12:30 for booking

* Deliberately skip test for testing purposes

* slotInterval overrides intervals, but not duration

* Fix calendar mocking

* Another 45m length

* Unskip :(

* Fix test

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-01-11 17:33:34 +00:00
Udit TakkarandPeer Richelsen b34a568a93 fix: show recordings even to free plan users (#6411)
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>
2023-01-11 09:58:50 -07:00
sean-brydon a669403604 Fix avatars not overlapping (#6405)
* Fix avatars being weird

* Fix useravatar import

* Fix type error

* Fixing spacing
2023-01-11 15:00:46 +00:00
Peer Richelsen 7b2bfc2a64 added GITHUB_API_REPO_TOKEN for console (#6408) 2023-01-11 14:47:37 +00:00
Alex van Andel 7655c0c867 Testing in isolation 2023-01-11 13:24:02 +00:00
Nafees Nazikandsean-brydon 6fbabb4b26 fix: remove border (#6391)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-01-11 10:28:57 +00:00
sean-brydon edfc9ff082 Fixes ultrawide breaking booking limits selects (#6396) 2023-01-11 10:16:04 +00:00
Leo Giovanetti 59a9a77e3f Fixes cancelling all recurring events (#6384) 2023-01-10 14:09:28 -07:00
Joe Au-Yeungandkodiakhq[bot] 8e756d7648 Fix custom event names in emails (#6375)
* Fix What for normal bookings

* Fix subject on bookings with seats

* WIP

* Change more strings

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-10 18:32:50 +00:00
Udit Takkarandsean-brydon 5763c1c2f1 fix: use default length in form (#6378)
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: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-01-10 17:25:09 +00:00
Alex van Andel 6c5ae0b4d9 Fixed default value for ordering correctly (#6372) 2023-01-10 17:06:53 +01:00
9cb9ce2e42 Chor/bulk UI migration (#6367)
* Big bulk commit

* Switch and settings toggle

* Popover Kbar Timezones

* Image Uploader

* Fix core export

* Meta

* Swatch

* Remove shell

* Fix shell imports

* Moved ShellSubHeading component to UI, to prevent recursive imports from shell component

* Removed shell from ui ts config since shell doesnt have a dependency on shell anymore.

Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Omar López <zomars@me.com>
2023-01-10 15:39:29 +00:00
Omar LópezandCrowdin Bot 018be43017 New Crowdin translations by Github Action (#6365)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-01-10 08:11:59 -07:00
zomars 4f720c8c9b Update crowdin.yml 2023-01-10 08:05:14 -07:00
Udit TakkarandPeer Richelsen 06e1e113f0 fix: margin and padding (#6358)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-10 12:44:30 +00:00
sean-brydonandOmar López 3fc6f38da9 Chor/more UI migrations (#6353)
* Move tabs to components

* Lots of form components

* Update apps/web/components/apps/layouts/InstalledAppsLayout.tsx

Co-authored-by: Omar López <zomars@me.com>
2023-01-10 12:25:39 +00:00
Omar López 8e8c96c267 v2.4.6
release.yml / release (push) Canceled after 0s
2023-01-09 20:26:21 -07:00
github-actions[bot]andCrowdin Bot fab765c5ee New Crowdin translations by Github Action (#6349)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-01-09 20:25:22 -07:00
zomars 89ed07a097 Update crowdin.yml 2023-01-09 20:17:03 -07:00
zomars d7d73d2caf Update crowdin.yml 2023-01-09 20:15:53 -07:00
zomars aa92730e28 Fix labeler 2023-01-09 19:52:02 -07:00
430430e0cd Feedback of reusable wysiwyg editor (#6357)
* make text editor reusable

* use dropdown component for block type

* remove ring when clicking on block type

* allow excluding items in toolbar

* refactor code

* make AddVariableDropdown reusable

* fixed missed translations

* fixes variable translation issue

* change missed translation variables

* make AddVariablesDropdown reusable

* reorder block types in dropdown

* remove not needed prop

* code clean up

* center add variable dropwdown icon

* remove rounded borders in dropwdown on hover and active

* center icons

* fix tool bar items dropdown

* add constant for dynamic text variables

* remove not needed variables (added from merge)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
2023-01-09 19:22:17 -07:00
62713940b0 Chor/v2 more tidyup - Card - link icon button - tooltip (#6312)
* Card

* Step card

* Already in components

* Link Icon Button

* Tooltip

* Fix types

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-09 19:22:04 -07:00
75aef09338 Google Meet - installable app (#5904)
* Abstract app category navigation

* Send key schema to frontend

Co-authored-by: Omar López <zomars@users.noreply.github.com>

* Render keys for apps on admin

* Add enabled col for apps

* Save app keys to DB

* Add checks for admin role

* Abstract setup components

* Add AdminAppsList to setup wizard

* Migrate to v10 tRPC

* Default hide keys

* Display enabled apps

* Merge branch 'main' into admin-apps-ui

* Toggle calendars

* WIP

* Add params and include AppCategoryNavigation

* Refactor getEnabledApps

* Add warning for disabling apps

* Fallback to cal video when a video app is disabled

* WIP send disabled email

* Send email to all users of  event types with payment app

* Disable Stripe when app is disabled

* Disable apps in event types

* Send email to users on disabled apps

* Send email based on what app was disabled

* WIP type fix

* Disable navigation to apps list if already setup

* UI import fixes

* Waits for session data before redirecting

* Updates admin seeded password

To comply with admin password requirements

* Update yarn.lock

* Flex fixes

* Adds admin middleware

* Clean up

* WIP

* WIP

* NTS

* Add dirName to app metadata

* Upsert app if not in db

* Upsert app if not in db

* Add dirName to app metadata

* Add keys to app packages w/ keys

* Merge with main

* Toggle show keys & on enable

* Fix empty keys

* Fix lark calendar metadata

* Fix some type errors

* Fix Lark metadata & check for category when upserting

* More type fixes

* Fix types & add keys to google cal

* WIP

* WIP

* WIP

* More type fixes

* Fix type errors

* Fix type errors

* More type fixes

* More type fixes

* More type fixes

* Feedback

* Fixes default value

* Feedback

* Migrate credential invalid col default value "false"

* Upsert app on saving keys

* Clean up

* Validate app keys on frontend

* Add nonempty to app keys schemas

* Add web3

* Listlocale filter on categories / category

* Grab app metadata via category or categories

* Show empty screen if no apps are enabled

* Fix type checks

* Fix type checks

* Fix type checks

* Fix type checks

* Fix type checks

* Fix type checks

* Replace .nonempty() w/ .min(1)

* Fix type error

* Address feedback

* Draft Google Meet install button

* Add install button and warning dialog

* WIP

* WIP

* Display warning when Meet is selected

* Display Google Meet warning on email to organizer

* Fix email

* Fix type errors

* Fix type error

* Add connected account component

* Add warning message

* Address comments

* Address feedback

* Clean up & add MeetLocationType

* Use useApp hook

* Translate to new API approach

* Remove console.log

* Refactor

* Fix missing backup Cal video link

* WIP

* Address feedback

* Update submodules

* Feedback

* Submodule sync

Co-authored-by: Omar López <zomars@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2023-01-10 02:01:57 +00:00
4e65c30e18 fix: Parameterize Stripe Payment Fee configs (#6024)
* Parameterize Stripe Payment Fee configs

* Restore constant values for Payment Fee params

* Restore constant values for all Payment Fee params

* Remove STRIPE_ prefix on payment fee vars

* Cast Payment Fee params as Number

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2023-01-09 18:50:49 -07:00
8b873e2828 Adding a bunch of performance markers (#6303)
* Adding a bunch of performance markers

* Update packages/lib/server/perfObserver.ts

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-09 18:42:46 -07:00
zomars e70d179628 Update website 2023-01-09 18:31:38 -07:00
Nafees Nazik ca9a836300 fix: remove padding from username field (#6356) 2023-01-09 19:35:00 +01:00
Alex van Andel abdf72dc0f Fixup yarn.lock 2023-01-09 14:36:49 +00:00
dependabot[bot]andBailey Pumfleet 227cd165f6 Bump json5 from 1.0.1 to 1.0.2 (#6351)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-01-09 14:19:51 +00:00
a0135cdc59 Make WYSIWYG editor reusable (#6293)
* make text editor reusable

* use dropdown component for block type

* remove ring when clicking on block type

* allow excluding items in toolbar

* refactor code

* make AddVariableDropdown reusable

* fixed missed translations

* fixes variable translation issue

* change missed translation variables

* make AddVariablesDropdown reusable

* reorder block types in dropdown

* remove not needed prop

* code clean up

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-09 14:15:11 +00:00
112d504148 Chor/v2 UI tidy - Dialog & Toasts (#6307)
* Dialog Tidyup

* Move toasts

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2023-01-09 13:57:12 +00:00
Peer Richelsen d275930732 link to event-types in settings to fix cal.com usage (#6348) 2023-01-09 13:15:10 +01:00
Alex van Andel 97e33bdd7e Set bg-white to secondary variant (#6339) 2023-01-08 17:50:37 +01:00
Hariom Balhara 3f13791faf Add app-store installed page navigationt ests (#6333) 2023-01-08 09:29:20 +01:00
Omar LópezandAlex van Andel 359cfc4be0 Replaces mdx-remote with markdown-it (#6326)
* Replaces mdx-remote with react-markdown

* Replaces react-markdown with already available markdown-it

* Renames readme to description

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-08 00:05:35 +00:00
Alex van AndelandPeer Richelsen 1ad6bba0ab set useQueryParam to false for app categories (#6330)
* set useQueryParam to false for app categories

* New param 'useQueryParam' for AppCategoryNavigation/AdminAppsList

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-07 23:21:21 +00:00
Alex van Andel 6dc4211d81 Fixes hydration error, uses useTypedQuery for tabName (#6327) 2023-01-07 22:42:30 +00:00
Alex van Andel e5f0452728 Don't initialize KBar when unauthed (#6328)
* Don't initialize KBar when unauthed

* Don't render trigger when query is undefined
2023-01-07 22:31:15 +00:00
zomars d95bba63f4 Submodule sync 2023-01-07 11:19:16 -07:00
zomars 6f48869021 Add missing slash to custom import 2023-01-07 10:17:38 -07:00
Jeff Loiselle 845bfae887 adds event types to kbar (#6210) 2023-01-07 10:06:53 -07:00
zomars 3fe02fba26 Update basic.e2e.ts 2023-01-07 09:55:30 -07:00
zomars 4ca5ca9fe2 Update hash-my-url.e2e.ts 2023-01-07 09:50:02 -07:00
Hariom BalharaandPeer Richelsen 364e187080 Fix/Next.js-13/Circular Dependency in Routing Forms (#6321)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-07 15:50:28 +00:00
Peer Richelsen b1193eef1e Update README.md 2023-01-07 16:27:56 +01:00
Peer Richelsen e110f12cf1 Update README.md 2023-01-07 16:26:47 +01:00
Peer Richelsen 022b513935 Update package.json 2023-01-07 11:55:50 +01:00
Jeff LoiselleandPeer Richelsen 010f39a7a4 adds render.com url constant (#6287)
* adds render.com url constant

* fixes webapp_url constant

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-07 10:28:02 +00:00
Peer Richelsen 6b6eed66eb fixed mobile sidebar in settings (#6317) 2023-01-07 10:10:35 +00:00
Udit Takkar c214e01f03 fix: use overflow-auto (#6295)
* fix: use overflow-auto

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

* fix: scroll issues with too many filters

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

* chore: merge conflict

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

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-07 11:00:11 +01:00
Carina WollendorferandCarinaWolli a28ac3634d Add start and end time of rescheduled booking to webhook payload (#6297)
* add start and end time of rescheduled booking

* fix type error

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-06 13:37:06 -07:00
zomars a0709fac88 Circular dependecy hotfix 2023-01-06 13:15:34 -07:00
Alex van Andel e5b6a3332e Feature/new design radio area group (#6289)
* Refactor of the RadioAreaGroup component

* Move RadioAreaGroup outside of v2/core

* Use useTypedQuery

* Removed unused (broken) imports

* Fix type for Select
2023-01-06 20:05:23 +00:00
Jeff Loiselle 979686920a fixes codespace login msg spelling (#6288) 2023-01-06 12:37:03 -07:00
zomars 793850f3aa Update console 2023-01-06 11:10:17 -07:00
Nafees NazikandPeer Richelsen fa9d72fc0a fix: flash of untranslated title in settings/my-account/profile (#6163)
* fix: flash of untranslated content

* feat: extract loading into a variable

* fix: use optional chaining because typescript complaining

* feat: render Meta component even if not loading for seo purpose

* fix: received an array with more than 1 element as
children warning

* fix: changes

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-06 10:35:32 -07:00
zomars 0d485ffed6 Updates website 2023-01-06 10:18:18 -07:00
Peer Richelsen 9a2e55d528 revert version change 2023-01-06 16:59:26 +01:00
github-actions[bot]andCrowdin Bot 81ecbb86a5 New Crowdin translations by Github Action (#6308)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-01-06 16:58:09 +01:00
Peer Richelsen 2cd816932b Update package.json 2023-01-06 16:55:01 +01:00
Peer Richelsenandzomars d83411162d Upgrade to next 13.1 (#6265)
Co-authored-by: zomars <zomars@me.com>
2023-01-06 12:13:56 +00:00
Nafees NazikandPeer Richelsen c9c54db3a1 fix: remove duplicate font preload (#6291)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-06 12:35:48 +01:00
Udit Takkarandkodiakhq[bot] d100600789 feat: update team schema, update team query (#6294)
team appearance section

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: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-06 10:55:57 +00:00
413 changed files with 5708 additions and 4534 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
🚀 Welcome to Cal.com! Try typing 'yarn dx' to get a quick dev envionment.
+9 -1
View File
@@ -31,11 +31,14 @@ NEXT_PUBLIC_CONSOLE_URL='http://localhost:3004'
NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js' NEXT_PUBLIC_EMBED_LIB_URL='http://localhost:3000/embed/embed.js'
# To enable SAML login, set both these variables # To enable SAML login, set both these variables
# @see https://github.com/calcom/cal.com/tree/main/packages/ee#setting-up-saml-login # @see https://github.com/calcom/cal.com/tree/main/packages/features/ee#setting-up-saml-login
# SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml" # SAML_DATABASE_URL="postgresql://postgres:@localhost:5450/cal-saml"
SAML_DATABASE_URL= SAML_DATABASE_URL=
# SAML_ADMINS='pro@example.com' # SAML_ADMINS='pro@example.com'
SAML_ADMINS= SAML_ADMINS=
# NEXT_PUBLIC_HOSTED_CAL_FEATURES=1
NEXT_PUBLIC_HOSTED_CAL_FEATURES=
# If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line. # If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then uncomment the follow line.
# @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js # @see https://devcenter.heroku.com/articles/connecting-heroku-postgres#connecting-in-node-js
# PGSSLMODE='no-verify' # PGSSLMODE='no-verify'
@@ -102,6 +105,8 @@ STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_WEBHOOK_SECRET= STRIPE_WEBHOOK_SECRET=
STRIPE_PRIVATE_KEY= STRIPE_PRIVATE_KEY=
STRIPE_CLIENT_ID= STRIPE_CLIENT_ID=
PAYMENT_FEE_FIXED=
PAYMENT_FEE_PERCENTAGE=
# Use for internal Public API Keys and optional # Use for internal Public API Keys and optional
API_KEY_PREFIX=cal_ API_KEY_PREFIX=cal_
@@ -147,3 +152,6 @@ SENTRY_IGNORE_API_RESOLUTION_ERROR=
NEXT_PUBLIC_APP_NAME="Cal.com" NEXT_PUBLIC_APP_NAME="Cal.com"
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com" NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc." NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
# Set this to true in to disable new signups
# NEXT_PUBLIC_DISABLE_SIGNUP=true
NEXT_PUBLIC_DISABLE_SIGNUP=
-6
View File
@@ -1,9 +1,3 @@
"♻️ autoupdate":
- apps/web/public/static/locales/**/*.json
automerge:
- apps/web/public/static/locales/**/*.json
"❗️ migrations": "❗️ migrations":
- packages/prisma/migrations/**/migration.sql - packages/prisma/migrations/**/migration.sql
+3
View File
@@ -9,6 +9,8 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
required-checks:
uses: calcom/cal.com/.github/workflows/required-checks.yml@main
synchronize-with-crowdin: synchronize-with-crowdin:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -21,6 +23,7 @@ jobs:
with: with:
upload_translations: true upload_translations: true
download_translations: true download_translations: true
pull_request_labels: "♻️ autoupdate, automerge"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
+1
View File
@@ -7,6 +7,7 @@ on:
paths: paths:
- apps/web/** - apps/web/**
- packages/embeds/** - packages/embeds/**
workflow_call:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
+1 -2
View File
@@ -1,13 +1,12 @@
name: E2E test name: E2E test
on: on:
push:
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
pull_request_target: # So we can test on forks pull_request_target: # So we can test on forks
branches: branches:
- main - main
paths: paths:
- apps/web/** - apps/web/**
- packages/** - packages/**
workflow_call:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
+2
View File
@@ -14,3 +14,5 @@ jobs:
- uses: actions/labeler@v4 - uses: actions/labeler@v4
with: with:
repo-token: "${{ secrets.GITHUB_TOKEN }}" repo-token: "${{ secrets.GITHUB_TOKEN }}"
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
sync-labels: ""
+1 -2
View File
@@ -1,10 +1,9 @@
name: "Meta Workflow: Require Conditional Status Checks" name: "Meta Workflow: Require Conditional Status Checks"
on: on:
push:
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
pull_request: pull_request:
branches: branches:
- main - main
workflow_call:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
+3 -2
View File
@@ -1,13 +1,14 @@
name: Unit tests name: Unit tests
on: on:
push:
branches: [ feat/routing-forms/reporting ] # TODO: Remove this after merged in main
pull_request_target: # So we can test on forks pull_request_target: # So we can test on forks
branches: branches:
- main - main
paths: paths:
- apps/web/** - apps/web/**
- packages/** - packages/**
workflow_run:
workflows: [Crowdin Action]
types: [completed]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
+11
View File
@@ -315,6 +315,17 @@ Please see our [contributing guide](/CONTRIBUTING.md).
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process. We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
<!-- TRANSLATIONS -->
### Translations
Don't code but still want to contribute? Join our [slack](https://cal.com/slack) and join the [#i18n channel](https://calendso.slack.com/archives/C02BY67GMMW) and let us know what language you want to translate.
![ar translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ar&style=flat&logo=crowdin&query=%24.progress.0.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![bg translation](https://img.shields.io/badge/dynamic/json?color=blue&label=bg&style=flat&logo=crowdin&query=%24.progress.1.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![cs translation](https://img.shields.io/badge/dynamic/json?color=blue&label=cs&style=flat&logo=crowdin&query=%24.progress.2.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![de translation](https://img.shields.io/badge/dynamic/json?color=blue&label=de&style=flat&logo=crowdin&query=%24.progress.3.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![el translation](https://img.shields.io/badge/dynamic/json?color=blue&label=el&style=flat&logo=crowdin&query=%24.progress.4.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![en translation](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.progress.5.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![es translation](https://img.shields.io/badge/dynamic/json?color=blue&label=es&style=flat&logo=crowdin&query=%24.progress.6.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![es-419 translation](https://img.shields.io/badge/dynamic/json?color=blue&label=es-419&style=flat&logo=crowdin&query=%24.progress.7.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![fr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=fr&style=flat&logo=crowdin&query=%24.progress.8.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![he translation](https://img.shields.io/badge/dynamic/json?color=blue&label=he&style=flat&logo=crowdin&query=%24.progress.9.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![hu translation](https://img.shields.io/badge/dynamic/json?color=blue&label=hu&style=flat&logo=crowdin&query=%24.progress.10.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![it translation](https://img.shields.io/badge/dynamic/json?color=blue&label=it&style=flat&logo=crowdin&query=%24.progress.11.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ja translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.progress.12.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ko translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.progress.13.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![nl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=nl&style=flat&logo=crowdin&query=%24.progress.14.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![no translation](https://img.shields.io/badge/dynamic/json?color=blue&label=no&style=flat&logo=crowdin&query=%24.progress.15.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![pl translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pl&style=flat&logo=crowdin&query=%24.progress.16.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![pt translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt&style=flat&logo=crowdin&query=%24.progress.17.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![pt-BR translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-BR&style=flat&logo=crowdin&query=%24.progress.18.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ro translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ro&style=flat&logo=crowdin&query=%24.progress.19.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.progress.20.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![sr translation](https://img.shields.io/badge/dynamic/json?color=blue&label=sr&style=flat&logo=crowdin&query=%24.progress.21.data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-200011276-1.json) ![sv translation](https://img.shields.io/badge/dynamic/json?color=blue&label=sv&style=flat&logo=crowdin&query=%Line truncated
## Integrations ## Integrations
### Obtaining the Google API Credentials ### Obtaining the Google API Credentials
Submodule apps/api updated: 9c23a8e5ab...7aebdb8c96.
+1 -1
View File
@@ -17,7 +17,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"iframe-resizer-react": "^1.1.0", "iframe-resizer-react": "^1.1.0",
"next": "^12.3.1", "next": "^13.1.1",
"nextra": "^1.1.0", "nextra": "^1.1.0",
"nextra-theme-docs": "^1.2.2", "nextra-theme-docs": "^1.2.2",
"react": "^18.2.0", "react": "^18.2.0",
+3 -3
View File
@@ -31,7 +31,7 @@ All apps can be found under `packages/app-store`. In this folder is `_example` w
| ├──index.ts | ├──index.ts
| ├──package.json | ├──package.json
| ├──.env.example | ├──.env.example
| ├──README.mdx | ├──DESCRIPTION.md
``` ```
## Getting Started ## Getting Started
@@ -48,9 +48,9 @@ Under the `/components` folder, this is where the install button for your app sh
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file. The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
On the app store page you can customize your apps description by adding a markdown file called `README.mdx`. If you do not add one then the description from you `package.json` will be used instead. On the app store page you can customize your apps description by adding a markdown file called `DESCRIPTION.md`. If you do not add one then the description from you `package.json` will be used instead.
The `/static` folder is where you can store your app icon and any images that your `README.mdx` may use. The `/static` folder is where you can store your app icon and any images that your `DESCRIPTION.md` may use.
## Adding Your App to the App Store ## Adding Your App to the App Store
To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object. To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object.
+1 -1
View File
@@ -20,7 +20,7 @@
"@radix-ui/react-slider": "^1.0.0", "@radix-ui/react-slider": "^1.0.0",
"@radix-ui/react-switch": "^1.0.0", "@radix-ui/react-switch": "^1.0.0",
"@radix-ui/react-tooltip": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0",
"next": "^12.3.1", "next": "^13.1.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
+1 -1
View File
@@ -14,7 +14,7 @@
"dependencies": { "dependencies": {
"highlight.js": "^11.6.0", "highlight.js": "^11.6.0",
"isarray": "2.0.5", "isarray": "2.0.5",
"next": "^12.3.1", "next": "^13.1.1",
"openapi-snippet": "^0.13.0", "openapi-snippet": "^0.13.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
-9
View File
@@ -1,9 +0,0 @@
import { Suspense, SuspenseProps } from "react";
/**
* Wrapper around `<Suspense />` which will render the `fallback` when on server
* Can be simply replaced by `<Suspense />` once React 18 is ready.
*/
export const ClientSuspense = (props: SuspenseProps) => {
return <>{typeof window !== "undefined" ? <Suspense {...props} /> : props.fallback}</>;
};
+7 -7
View File
@@ -504,7 +504,7 @@ const tabs = [
return ( return (
<> <>
<div> <div>
<small className="flex py-4 text-neutral-500"> <small className="flex py-4 text-gray-500">
{t("place_where_cal_widget_appear", { appName: APP_NAME })} {t("place_where_cal_widget_appear", { appName: APP_NAME })}
</small> </small>
</div> </div>
@@ -556,7 +556,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
} }
return ( return (
<> <>
<small className="flex py-4 text-neutral-500">{t("create_update_react_component")}</small> <small className="flex py-4 text-gray-500">{t("create_update_react_component")}</small>
<TextArea <TextArea
data-testid="embed-react" data-testid="embed-react"
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>} ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
@@ -650,7 +650,7 @@ const ChooseEmbedTypesDialogContent = () => {
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white"> <div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
{embed.illustration} {embed.illustration}
</div> </div>
<div className="mt-2 font-medium text-neutral-900">{embed.title}</div> <div className="mt-2 font-medium text-gray-900">{embed.title}</div>
<p className="text-sm text-gray-500">{embed.subtitle}</p> <p className="text-sm text-gray-500">{embed.subtitle}</p>
</button> </button>
))} ))}
@@ -827,7 +827,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}> onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
<CollapsibleTrigger <CollapsibleTrigger
type="button" type="button"
className="flex w-full items-center text-base font-medium text-neutral-900"> className="flex w-full items-center text-base font-medium text-gray-900">
<div> <div>
{embedType === "inline" {embedType === "inline"
? "Inline Embed Customization" ? "Inline Embed Customization"
@@ -838,7 +838,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<Icon.FiChevronRight <Icon.FiChevronRight
className={`${ className={`${
isEmbedCustomizationOpen ? "rotate-90 transform" : "" isEmbedCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`} } ml-auto h-5 w-5 text-gray-500`}
/> />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent className="text-sm"> <CollapsibleContent className="text-sm">
@@ -1001,11 +1001,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
open={isBookingCustomizationOpen} open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}> onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleTrigger className="flex w-full" type="button"> <CollapsibleTrigger className="flex w-full" type="button">
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div> <div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
<Icon.FiChevronRight <Icon.FiChevronRight
className={`${ className={`${
isBookingCustomizationOpen ? "rotate-90 transform" : "" isBookingCustomizationOpen ? "rotate-90 transform" : ""
} ml-auto h-5 w-5 text-neutral-500`} } ml-auto h-5 w-5 text-gray-500`}
/> />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
+3 -3
View File
@@ -62,12 +62,12 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
const className = tab.className || ""; const className = tab.className || "";
return ( return (
<Component key={tab.name}> <Component key={tab.name}>
<Link key={tab.name} href={href} {...linkProps}> <Link key={tab.name} href={href} {...linkProps} legacyBehavior>
<a <a
onClick={onClick} onClick={onClick}
className={classNames( className={classNames(
isCurrent isCurrent
? "border-neutral-900 text-neutral-900" ? "border-neutral-900 text-gray-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700", : "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium", "group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
className className
@@ -76,7 +76,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
{tab.icon && ( {tab.icon && (
<tab.icon <tab.icon
className={classNames( className={classNames(
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500", isCurrent ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block" "-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
)} )}
aria-hidden="true" aria-hidden="true"
+2 -1
View File
@@ -1,6 +1,7 @@
import React, { ComponentProps } from "react"; import React, { ComponentProps } from "react";
import { ErrorBoundary, Icon, Shell } from "@calcom/ui"; import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary, Icon } from "@calcom/ui";
import NavTabs from "./NavTabs"; import NavTabs from "./NavTabs";
+6 -3
View File
@@ -6,12 +6,13 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components"; import { InstallAppButton } from "@calcom/app-store/components";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration"; import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired"; import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import Shell from "@calcom/features/shell/Shell";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants"; import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App"; import { App as AppType } from "@calcom/types/App";
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText, HeadSeo } from "@calcom/ui"; import { Button, Icon, showToast, SkeletonButton, SkeletonText, HeadSeo } from "@calcom/ui";
const Component = ({ const Component = ({
name, name,
@@ -98,8 +99,10 @@ const Component = ({
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1> <h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
</div> </div>
<h2 className="text-sm font-medium text-gray-600"> <h2 className="text-sm font-medium text-gray-600">
<Link href={`categories/${categories[0]}`}> <Link
<a className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">{categories[0]}</a> href={`categories/${categories[0]}`}
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
{categories[0]}
</Link>{" "} </Link>{" "}
• {t("published_by", { author })} • {t("published_by", { author })}
</h2> </h2>
@@ -13,10 +13,10 @@ import {
EmptyScreen, EmptyScreen,
Icon, Icon,
List, List,
ShellSubHeading,
showToast, showToast,
AppSkeletonLoader as SkeletonLoader, AppSkeletonLoader as SkeletonLoader,
Switch, Switch,
ShellSubHeading,
} from "@calcom/ui"; } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -184,9 +184,7 @@ function ConnectedCalendarsList(props: Props) {
<div className="border-t border-gray-200"> <div className="border-t border-gray-200">
{!fromOnboarding && ( {!fromOnboarding && (
<> <>
<p className="px-4 pt-4 text-sm text-neutral-500"> <p className="px-4 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
{t("toggle_calendars_conflict")}
</p>
<ul className="space-y-2 p-4"> <ul className="space-y-2 p-4">
{item.calendars.map((cal) => ( {item.calendars.map((cal) => (
<CalendarSwitch <CalendarSwitch
@@ -330,7 +328,10 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
})} })}
description={t(`no_category_apps_description_calendar`)} description={t(`no_category_apps_description_calendar`)}
buttonRaw={ buttonRaw={
<Button color="secondary" href="/apps/categories/calendar"> <Button
color="secondary"
data-testid="connect-calendar-apps"
href="/apps/categories/calendar">
{t(`connect_calendar_apps`)} {t(`connect_calendar_apps`)}
</Button> </Button>
} }
@@ -2,19 +2,9 @@ import { useSession } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { ComponentProps } from "react"; import React, { ComponentProps } from "react";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen, Icon, Shell, HorizontalTabs, HorizontalTabItemProps } from "@calcom/ui"; import { EmptyScreen, Icon } from "@calcom/ui";
const tabs: HorizontalTabItemProps[] = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
type AppsLayoutProps = { type AppsLayoutProps = {
children: React.ReactNode; children: React.ReactNode;
@@ -30,11 +20,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
if (session.status === "loading") return <></>; if (session.status === "loading") return <></>;
return ( return (
<Shell {...rest} actions={actions?.("hidden sm:block")}> <Shell {...rest} actions={actions?.("block")}>
<div className="flex flex-col xl:flex-row"> <div className="flex flex-col xl:flex-row">
<div className="block lg:hidden">
<HorizontalTabs tabs={tabs} actions={actions?.("ml-6 mr-4 mt-3 block sm:hidden")} />
</div>
<main className="w-full"> <main className="w-full">
{emptyStore ? ( {emptyStore ? (
<EmptyScreen <EmptyScreen
@@ -2,10 +2,10 @@ import React, { ComponentProps } from "react";
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation"; import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
import { InstalledAppVariants } from "@calcom/app-store/utils"; import { InstalledAppVariants } from "@calcom/app-store/utils";
import Shell from "@calcom/features/shell/Shell";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Icon, Shell } from "@calcom/ui"; import { Icon } from "@calcom/ui";
import type { HorizontalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabItem"; import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
import type { VerticalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/VerticalTabItem";
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [ const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{ {
@@ -44,13 +44,15 @@ export default function InstalledAppsLayout({
children, children,
...rest ...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) { }: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
const variant: typeof InstalledAppVariants[number] = "payment";
const query = trpc.viewer.integrations.useQuery({ const query = trpc.viewer.integrations.useQuery({
variant: InstalledAppVariants.payment, variant,
onlyInstalled: true, onlyInstalled: true,
}); });
let actualTabs = tabs; let actualTabs = tabs;
if (query.data?.items.length === 0) { if (query.data?.items.length === 0) {
actualTabs = tabs.filter((tab) => tab.name !== InstalledAppVariants.payment); actualTabs = tabs.filter((tab) => tab.name !== variant);
} }
return ( return (
@@ -3,7 +3,8 @@ import { useRouter } from "next/router";
import React, { ComponentProps, useEffect } from "react"; import React, { ComponentProps, useEffect } from "react";
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout"; import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
import { ErrorBoundary, Shell } from "@calcom/ui"; import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary } from "@calcom/ui";
import { UserPermissionRole } from ".prisma/client"; import { UserPermissionRole } from ".prisma/client";
+24 -24
View File
@@ -87,7 +87,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
slug: eventTypeSlug, slug: eventTypeSlug,
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */ /** Treat as recurring only when a count exist and it's not a rescheduling workflow */
count: recurringCount && !rescheduleUid ? recurringCount : undefined, count: recurringCount && !rescheduleUid ? recurringCount : undefined,
ethSignature, ...(ethSignature ? { ethSignature } : {}),
}, },
}; };
@@ -114,29 +114,29 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>} {!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
</div> </div>
) : ( ) : (
<Link href={bookingUrl} prefetch={false}> <Link
<a href={bookingUrl}
className={classNames( prefetch={false}
"text-primary-500 hover:border-gray-900 hover:bg-gray-50", className={classNames(
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent dark:text-neutral-200", "text-primary-500 hover:border-gray-900 hover:bg-gray-50",
brand === "#fff" || brand === "#ffffff" ? "" : "" "dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand dark:text-darkgray-800 mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent",
)} brand === "#fff" || brand === "#ffffff" ? "" : ""
data-testid="time"> )}
{dayjs(slot.time).tz(timeZone()).format(timeFormat)} data-testid="time">
{!!seatsPerTimeSlot && ( {dayjs(slot.time).tz(timeZone()).format(timeFormat)}
<p {!!seatsPerTimeSlot && (
className={`${ <p
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8 className={`${
? "text-rose-600" slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33 ? "text-rose-600"
? "text-yellow-500" : slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
: "text-emerald-400" ? "text-yellow-500"
} text-sm`}> : "text-emerald-400"
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "} } text-sm`}>
{seatsPerTimeSlot} {t("seats_available")} {slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
</p> {seatsPerTimeSlot} {t("seats_available")}
)} </p>
</a> )}
</Link> </Link>
)} )}
</div> </div>
@@ -76,7 +76,7 @@ const BookingDescription: FC<Props> = (props) => {
profile={profile} profile={profile}
users={eventType.users} users={eventType.users}
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN} showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
size={10} size="sm"
truncateAfter={3} truncateAfter={3}
/> />
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300"> <h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
@@ -356,7 +356,7 @@ function BookingListItem(booking: BookingItemProps) {
<div <div
title={booking.title} title={booking.title}
className={classNames( className={classNames(
"max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-neutral-900 md:max-w-full", "max-w-10/12 sm:max-w-56 text-sm font-medium leading-6 text-gray-900 md:max-w-full",
isCancelled ? "line-through" : "" isCancelled ? "line-through" : ""
)}> )}>
{booking.title} {booking.title}
@@ -55,7 +55,7 @@ export default function CancelBooking(props: Props) {
placeholder={t("cancellation_reason_placeholder")} placeholder={t("cancellation_reason_placeholder")}
value={cancellationReason} value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)} onChange={(e) => setCancellationReason(e.target.value)}
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white " className="dark:bg-darkgray-100 dark:border-darkgray-400 mt-2 mb-4 w-full dark:text-white "
rows={3} rows={3}
/> />
<div className="flex flex-col-reverse rtl:space-x-reverse "> <div className="flex flex-col-reverse rtl:space-x-reverse ">
+2 -4
View File
@@ -1,6 +1,5 @@
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { AvatarGroup, AvatarGroupProps } from "@calcom/ui";
import AvatarGroup, { AvatarGroupProps } from "@components/ui/AvatarGroup";
export const UserAvatars = ({ export const UserAvatars = ({
profile, profile,
@@ -14,7 +13,6 @@ export const UserAvatars = ({
const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers; const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers;
return ( return (
<AvatarGroup <AvatarGroup
border="border-2 dark:border-darkgray-100 border-white"
items={ items={
[ [
{ {
@@ -38,7 +36,7 @@ export const UserAvatars = ({
href?: string; href?: string;
}[] }[]
} }
size={props.size} size="sm"
truncateAfter={props.truncateAfter} truncateAfter={props.truncateAfter}
/> />
); );
@@ -109,7 +109,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<div className="min-w-4 mb-2"> <div className="min-w-4 mb-2">
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-neutral-700"> <label htmlFor="availability" className="mt-0 flex text-sm font-medium text-gray-700">
{t("availability")} {t("availability")}
</label> </label>
</div> </div>
@@ -93,10 +93,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex justify-between"> <div className="flex justify-between">
<Label>{t("add_to_calendar")}</Label> <Label>{t("add_to_calendar")}</Label>
<Link href="/apps/categories/calendar"> <Link
<a target="_blank" className="text-sm text-gray-600 hover:text-gray-900"> href="/apps/categories/calendar"
{t("add_another_calendar")} target="_blank"
</a> className="text-sm text-gray-600 hover:text-gray-900">
{t("add_another_calendar")}
</Link> </Link>
</div> </div>
<div className="-mt-1 w-full"> <div className="-mt-1 w-full">
@@ -266,12 +267,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/> />
{/*TODO: Extract it out into a component when used more than once*/} {/*TODO: Extract it out into a component when used more than once*/}
{!IS_SELF_HOSTED && ( {!IS_SELF_HOSTED && (
<Link href="https://cal.com/pricing"> <Link href="https://cal.com/pricing" target="_blank">
<a target="_blank"> <Badge variant="orange" className="ml-2">
<Badge variant="orange" className="ml-2"> Platform Only
Platform Only </Badge>
</Badge>
</a>
</Link> </Link>
)} )}
</div> </div>
+95 -113
View File
@@ -1,8 +1,8 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group"; import * as RadioGroup from "@radix-ui/react-radio-group";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useRef, useState } from "react"; import React, { useEffect, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form"; import { Controller, useFormContext, UseFormRegisterReturn, useWatch } from "react-hook-form";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType"; import convertToNewDurationType, { DurationType } from "@calcom/lib/convertToNewDurationType";
@@ -12,13 +12,95 @@ import { PeriodType } from "@calcom/prisma/client";
import type { BookingLimit } from "@calcom/types/Calendar"; import type { BookingLimit } from "@calcom/types/Calendar";
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui"; import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
const MinimumBookingNoticeInput = React.forwardRef<
HTMLInputElement,
Omit<UseFormRegisterReturn<"minimumBookingNotice">, "ref">
>(function MinimumBookingNoticeInput({ ...passThroughProps }, ref) {
const { t } = useLocale();
const { setValue, getValues } = useFormContext<FormValues>();
const durationTypeOptions: {
value: DurationType;
label: string;
}[] = [
{
label: t("minutes"),
value: "minutes",
},
{
label: t("hours"),
value: "hours",
},
{
label: t("days"),
value: "days",
},
];
const [minimumBookingNoticeDisplayValues, setMinimumBookingNoticeDisplayValues] = useState<{
type: DurationType;
value: number;
}>({
type: findDurationType(getValues(passThroughProps.name)),
value: convertToNewDurationType(
"minutes",
findDurationType(getValues(passThroughProps.name)),
getValues(passThroughProps.name)
),
});
// keep hidden field in sync with minimumBookingNoticeDisplayValues
useEffect(() => {
setValue(
passThroughProps.name,
convertToNewDurationType(
minimumBookingNoticeDisplayValues.type,
"minutes",
minimumBookingNoticeDisplayValues.value
)
);
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
return (
<div className="flex items-end justify-end">
<div className="w-1/2 md:w-full">
<InputField
required
defaultValue={minimumBookingNoticeDisplayValues.value}
onChange={(e) =>
setMinimumBookingNoticeDisplayValues({
...minimumBookingNoticeDisplayValues,
value: parseInt(e.target.value || "0", 10),
})
}
label={t("minimum_booking_notice")}
type="number"
placeholder="0"
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
/>
<input type="hidden" ref={ref} {...passThroughProps} />
</div>
<Select
isSearchable={false}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
defaultValue={durationTypeOptions.find(
(option) => option.value === minimumBookingNoticeDisplayValues.type
)}
onChange={(input) => {
if (input) {
setMinimumBookingNoticeDisplayValues({
...minimumBookingNoticeDisplayValues,
type: input.value,
});
}
}}
options={durationTypeOptions}
/>
</div>
);
});
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => { export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const { t } = useLocale(); const { t } = useLocale();
const formMethods = useFormContext<FormValues>(); const formMethods = useFormContext<FormValues>();
const minimumBookingNoticeType = useRef<DurationType>(findDurationType(eventType.minimumBookingNotice));
const prevBookingNoticeType = useRef<DurationType>(minimumBookingNoticeType.current);
const minimumBookingNoticeInDurationTypeFormValue = formMethods.watch("minimumBookingNoticeInDurationType");
const PERIOD_TYPES = [ const PERIOD_TYPES = [
{ {
@@ -49,59 +131,8 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
defaultValue: periodType?.type, defaultValue: periodType?.type,
}); });
const onMinimumNoticeDurationTypeChange = useMemo(
() => (durationType?: DurationType) => {
if (typeof durationType === "undefined") return;
// Store current selected type in ref to use in previous run for comparrison.
minimumBookingNoticeType.current = durationType;
// Uses the previous selected type and converts the value to the new type.
// Eg day was selected before, user selects hours now, so we multiple * 24 and round up.
const minimumBookingNoticeInDurationType = convertToNewDurationType(
prevBookingNoticeType.current,
durationType,
Number(minimumBookingNoticeInDurationTypeFormValue)
);
// Uses the round up value in the new type to calculate the value in minutes.
// We should NOT use minimumBookingNoticeInDurationTypeFormValue here, since that
// would break in the case that the user enters 90 minutes, changes the type to
// hours (that will show 2 hours), but the value in minutes would then remain 90,
// which is something else than the user things they save.
const minimumBookingNoticeInMinutes = convertToNewDurationType(
durationType,
"minutes",
Number(minimumBookingNoticeInDurationType)
);
// Updates both form values as well as stores the current type as previous type in ref.
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
formMethods.setValue("minimumBookingNoticeInDurationType", minimumBookingNoticeInDurationType);
prevBookingNoticeType.current = durationType;
},
[formMethods, minimumBookingNoticeInDurationTypeFormValue]
);
/**
* When the user inputs a new value for minimumBookingNoticeInDurationType,
* we calculate the value in minutes, and update this hidden field as well.
*/
const onMinimumNoticeChange = useMemo(
() => (duration: string) => {
const minimumBookingNoticeInMinutes = convertToNewDurationType(
minimumBookingNoticeType.current,
"minutes",
Number(duration)
);
formMethods.setValue("minimumBookingNotice", minimumBookingNoticeInMinutes);
},
[formMethods]
);
return ( return (
<div> <div className="space-y-8">
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4"> <div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="w-full"> <div className="w-full">
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label> <Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
@@ -168,56 +199,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/> />
</div> </div>
</div> </div>
<div className="flex flex-col space-y-4 pt-4 lg:flex-row lg:space-y-0 lg:space-x-4"> <div className="flex flex-col lg:flex-row lg:space-y-0 lg:space-x-4">
<div className="flex w-full items-end justify-end"> <div className="w-full">
<Controller <Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
name="minimumBookingNotice" <MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
control={formMethods.control}
render={() => {
const durationTypeOptions = [
{
label: t("minutes"),
value: "minutes",
},
{
label: t("hours"),
value: "hours",
},
{
label: t("days"),
value: "days",
},
];
return (
<>
<div className="w-1/2 md:w-3/4">
<InputField
required
label={t("minimum_booking_notice")}
type="number"
placeholder="120"
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
{...formMethods.register("minimumBookingNoticeInDurationType", {
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
onMinimumNoticeChange(event.target.value),
})}
/>
<input type="hidden" {...formMethods.register("minimumBookingNotice")} />
</div>
<Select
isSearchable={false}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
defaultValue={durationTypeOptions.find(
(option) => option.value === minimumBookingNoticeType.current
)}
onChange={(input) => onMinimumNoticeDurationTypeChange(input?.value as DurationType)}
options={durationTypeOptions}
/>
</>
);
}}
/>
</div> </div>
<div className="w-full"> <div className="w-full">
<Label htmlFor="slotInterval">{t("slot_interval")} </Label> <Label htmlFor="slotInterval">{t("slot_interval")} </Label>
@@ -252,9 +237,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/> />
</div> </div>
</div> </div>
<hr />
<hr className="my-8" />
<Controller <Controller
name="bookingLimits" name="bookingLimits"
control={formMethods.control} control={formMethods.control}
@@ -276,8 +259,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</SettingsToggle> </SettingsToggle>
)} )}
/> />
<hr />
<hr className="my-8" />
<Controller <Controller
name="periodType" name="periodType"
control={formMethods.control} control={formMethods.control}
@@ -1,16 +1,18 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js"; import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next";
import Link from "next/link";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useState } from "react"; import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form"; import { Controller, useForm, useFormContext } from "react-hook-form";
import { MultiValue } from "react-select"; import { MultiValue } from "react-select";
import { z } from "zod"; import { z } from "zod";
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations"; import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui"; import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
import { slugify } from "@lib/slugify"; import { slugify } from "@lib/slugify";
@@ -195,6 +197,23 @@ export const EventSetupTab = (
</li> </li>
); );
})} })}
{validLocations.some((location) => location.type === MeetLocationType) && (
<div className="flex text-sm text-gray-600">
<Icon.FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
<Trans i18nKey="event_type_requres_google_cal">
<p>
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
Change it{" "}
<Link
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
className="underline">
here.
</Link>{" "}
We will fall back to Cal video if you do not change it.
</p>
</Trans>
</div>
)}
{validLocations.length > 0 && validLocations.length !== locationOptions.length && ( {validLocations.length > 0 && validLocations.length !== locationOptions.length && (
<li> <li>
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}> <Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
@@ -326,6 +345,7 @@ export const EventSetupTab = (
<Skeleton as={Label} loadingClassName="w-16"> <Skeleton as={Label} loadingClassName="w-16">
{t("location")} {t("location")}
</Skeleton> </Skeleton>
<Controller <Controller
name="locations" name="locations"
control={formMethods.control} control={formMethods.control}
+135 -36
View File
@@ -1,9 +1,12 @@
import { SchedulingType } from "@prisma/client/"; import { SchedulingType } from "@prisma/client";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo } from "react"; import { ComponentProps } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext, useWatch, Control } from "react-hook-form";
import { Options } from "react-select";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect"; import CheckedTeamSelect, {
CheckedSelectOption,
} from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Label, Select } from "@calcom/ui"; import { Label, Select } from "@calcom/ui";
@@ -22,14 +25,66 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
email, email,
}); });
export const EventTeamTab = ({ const FixedHosts = ({
eventType, control,
team, labelText,
teamMembers, placeholder,
}: Pick<EventTypeSetupProps, "eventType" | "teamMembers" | "team">) => { options = [],
...rest
}: {
control: Control<FormValues>;
labelText: string;
placeholder: string;
options?: Options<CheckedSelectOption>;
} & Partial<ComponentProps<typeof CheckedTeamSelect>>) => {
return (
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{labelText}</Label>
<Controller
name="hostsFixed"
control={control}
render={({ field: { onChange, value } }) => {
return (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) => {
onChange(
options.map((option) => ({
isFixed: true,
userId: parseInt(option.value, 10),
}))
);
}}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
);
}}
/>
</div>
</div>
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
const formMethods = useFormContext<FormValues>(); const formMethods = useFormContext<FormValues>();
const { t } = useLocale(); const { t } = useLocale();
const schedulingType = useWatch({
control: formMethods.control,
name: "schedulingType",
});
const schedulingTypeOptions: { const schedulingTypeOptions: {
value: SchedulingType; value: SchedulingType;
label: string; label: string;
@@ -47,14 +102,13 @@ export const EventTeamTab = ({
}, },
]; ];
const teamMembersToValues = useMemo(() => { const teamMembersOptions = teamMembers.map(mapUserToValue);
return teamMembers.map(mapUserToValue);
}, [teamMembers]);
return ( return (
<div> <div>
{team && ( {team && (
<div className="space-y-3"> <div className="space-y-5">
<div className="flex flex-col pb-8"> <div className="flex flex-col">
<Label>{t("scheduling_type")}</Label> <Label>{t("scheduling_type")}</Label>
<Controller <Controller
name="schedulingType" name="schedulingType"
@@ -72,30 +126,75 @@ export const EventTeamTab = ({
/> />
</div> </div>
<div className="flex flex-col"> {schedulingType === SchedulingType.COLLECTIVE && (
<Label>{t("team")}</Label> <FixedHosts
<Controller options={teamMembersOptions}
name="users" placeholder={t("add_attendees")}
labelText={t("team")}
control={formMethods.control} control={formMethods.control}
defaultValue={eventType.users.map((user) => user.id.toString())}
render={({ field: { onChange, value } }) => (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) => onChange(options.map((user) => user.value))}
value={value
.map(
(userId) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
teamMembers.map(mapUserToValue).find((member) => member.value === userId)!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={teamMembersToValues}
placeholder={t("add_attendees")}
/>
)}
/> />
</div> )}
{schedulingType === SchedulingType.ROUND_ROBIN && (
<>
<FixedHosts
options={teamMembersOptions}
isOptionDisabled={(option) =>
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
control={formMethods.control}
/>
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{t("round_robin_hosts")}</Label>
<Controller
name="hosts"
control={formMethods.control}
render={({ field: { onChange, value } }) => (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) =>
onChange(
options.map((option) => ({
isFixed: false,
userId: parseInt(option.value, 10),
}))
)
}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
teamMembers
.map(mapUserToValue)
.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={teamMembers.map(mapUserToValue)}
isOptionDisabled={(option) =>
!!formMethods
.getValues("hostsFixed")
.find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_attendees")}
/>
)}
/>
</div>
{/*<TextField
required
type="number"
label={t("minimum_round_robin_hosts_count")}
defaultValue={1}
{...formMethods.register("minimumHostCount")}
addOnSuffix={<>{t("hosts")}</>}
/>*/}
</div>
</>
)}
</div> </div>
)} )}
</div> </div>
@@ -1,10 +1,11 @@
import { TFunction } from "next-i18next"; import { TFunction } from "next-i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useState } from "react"; import { useMemo, useState, Suspense } from "react";
import { UseFormReturn } from "react-hook-form"; import { UseFormReturn } from "react-hook-form";
import { TbWebhook } from "react-icons/tb"; import { TbWebhook } from "react-icons/tb";
import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -15,15 +16,15 @@ import {
ButtonGroup, ButtonGroup,
ConfirmationDialogContent, ConfirmationDialogContent,
Dialog, Dialog,
Divider, DropdownMenuSeparator,
Dropdown, Dropdown,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownItem,
DropdownMenuTrigger, DropdownMenuTrigger,
HorizontalTabs, HorizontalTabs,
Icon, Icon,
Label, Label,
Shell,
showToast, showToast,
Skeleton, Skeleton,
Switch, Switch,
@@ -32,7 +33,6 @@ import {
VerticalTabs, VerticalTabs,
} from "@calcom/ui"; } from "@calcom/ui";
import { ClientSuspense } from "@components/ClientSuspense";
import { EmbedButton, EmbedDialog } from "@components/Embed"; import { EmbedButton, EmbedDialog } from "@components/Embed";
type Props = { type Props = {
@@ -56,12 +56,15 @@ function getNavigation(props: {
installedAppsNumber: number; installedAppsNumber: number;
}) { }) {
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props; const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
const duration =
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
return [ return [
{ {
name: "event_setup_tab_title", name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`, href: `/event-types/${eventType.id}?tabName=setup`,
icon: Icon.FiLink, icon: Icon.FiLink,
info: `${eventType.length} Mins`, // TODO: Get this from props info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
}, },
{ {
name: "availability", name: "availability",
@@ -152,7 +155,7 @@ function EventTypeSingleLayout({
// If there is a team put this navigation item within the tabs // If there is a team put this navigation item within the tabs
if (team) { if (team) {
navigation.splice(2, 0, { navigation.splice(2, 0, {
name: "scheduling_type", name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`, href: `/event-types/${eventType.id}?tabName=team`,
icon: Icon.FiUsers, icon: Icon.FiUsers,
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin", info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
@@ -247,32 +250,36 @@ function EventTypeSingleLayout({
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-gray-100">
<Button <DropdownItem
color="minimal"
StartIcon={Icon.FiExternalLink}
target="_blank" target="_blank"
type="button"
StartIcon={Icon.FiExternalLink}
href={permalink} href={permalink}
rel="noreferrer" rel="noreferrer">
className="min-w-full">
{t("preview")} {t("preview")}
</Button> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-gray-100">
<Button color="minimal" StartIcon={Icon.FiLink}> <DropdownItem
type="button"
StartIcon={Icon.FiLink}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}>
{t("copy_link")} {t("copy_link")}
</Button> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="focus:ring-gray-100"> <DropdownMenuItem className="focus:ring-gray-100">
<Button <DropdownItem
color="minimal" type="button"
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
disabled={!hasPermsToDelete} disabled={!hasPermsToDelete}
onClick={() => setDeleteDialogOpen(true)} onClick={() => setDeleteDialogOpen(true)}>
className="min-w-full">
{t("delete")} {t("delete")}
</Button> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<Divider /> <DropdownMenuSeparator className="block sm:hidden" />
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100"> <div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
<Skeleton <Skeleton
as={Label} as={Label}
@@ -301,7 +308,7 @@ function EventTypeSingleLayout({
</Button> </Button>
</div> </div>
}> }>
<ClientSuspense fallback={<Icon.FiLoader />}> <Suspense fallback={<Icon.FiLoader />}>
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8"> <div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
<div className="hidden xl:block"> <div className="hidden xl:block">
<VerticalTabs <VerticalTabs
@@ -324,7 +331,7 @@ function EventTypeSingleLayout({
</div> </div>
</div> </div>
</div> </div>
</ClientSuspense> </Suspense>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent <ConfirmationDialogContent
isLoading={deleteMutation.isLoading} isLoading={deleteMutation.isLoading}
@@ -57,7 +57,7 @@ export default function RecurringEventController({
{recurringEventState && ( {recurringEventState && (
<div data-testid="recurring-event-collapsible" className="text-sm"> <div data-testid="recurring-event-collapsible" className="text-sm">
<div className="flex items-center"> <div className="flex items-center">
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p> <p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
<input <input
type="number" type="number"
min="1" min="1"
@@ -89,7 +89,7 @@ export default function RecurringEventController({
/> />
</div> </div>
<div className="mt-4 flex items-center"> <div className="mt-4 flex items-center">
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p> <p className="text-gray-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
<input <input
type="number" type="number"
min="1" min="1"
@@ -105,7 +105,7 @@ export default function RecurringEventController({
setRecurringEventState(newVal); setRecurringEventState(newVal);
}} }}
/> />
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2"> <p className="text-gray-900 ltr:mr-2 rtl:ml-2">
{t("events", { {t("events", {
count: recurringEventState.count, count: recurringEventState.count,
})} })}
@@ -16,7 +16,7 @@ const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps)
<> <>
<div className="mt-6 flex flex-row"> <div className="mt-6 flex flex-row">
<div className="w-full"> <div className="w-full">
<label htmlFor="createEventsOn" className="flex text-sm font-medium text-neutral-700"> <label htmlFor="createEventsOn" className="flex text-sm font-medium text-gray-700">
{t("create_events_on")} {t("create_events_on")}
</label> </label>
<div className="mt-2"> <div className="mt-2">
@@ -19,7 +19,8 @@ interface IUserSettingsProps {
const UserSettings = (props: IUserSettingsProps) => { const UserSettings = (props: IUserSettingsProps) => {
const { user, nextStep } = props; const { user, nextStep } = props;
const { t } = useLocale(); const { t } = useLocale();
const [selectedTimeZone, setSelectedTimeZone] = useState(user.timeZone ?? dayjs.tz.guess()); const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
const { const {
register, register,
handleSubmit, handleSubmit,
+2 -2
View File
@@ -22,8 +22,8 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"} imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
/> />
<section className="line-clamp-4 mt-2 w-full space-y-1"> <section className="line-clamp-4 mt-2 w-full space-y-1">
<p className="font-medium text-neutral-900 dark:text-white">{member.name}</p> <p className="font-medium text-gray-900 dark:text-white">{member.name}</p>
<p className="line-clamp-3 overflow-ellipsis text-sm font-normal text-neutral-500 dark:text-white"> <p className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white">
{member.bio || t("user_from_team", { user: member.name, team: teamName })} {member.bio || t("user_from_team", { user: member.name, team: teamName })}
</p> </p>
</section> </section>
+2 -2
View File
@@ -24,7 +24,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
)} )}
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}> <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>} {props.heading && <h2 className="font-cal text-center text-3xl text-gray-900">{props.heading}</h2>}
</div> </div>
{props.loading && ( {props.loading && (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50"> <div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
@@ -35,7 +35,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10"> <div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
{props.children} {props.children}
</div> </div>
<div className="mt-8 text-center text-sm text-neutral-600"> <div className="mt-8 text-center text-sm text-gray-600">
{props.footerText} {props.footerText}
<Credits /> <Credits />
</div> </div>
+1 -7
View File
@@ -35,13 +35,7 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
return ( return (
<li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2"> <li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2">
{item.href ? ( {item.href ? <Link href={item.href}>{avatar}</Link> : avatar}
<Link href={item.href}>
<a>{avatar}</a>
</Link>
) : (
avatar
)}
</li> </li>
); );
} }
+1 -1
View File
@@ -13,7 +13,7 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
type="button" type="button"
{...props} {...props}
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900"> className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
<props.Icon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" /> <props.Icon className="h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
{props.children} {props.children}
</button> </button>
</div> </div>
-22
View File
@@ -1,22 +0,0 @@
import Link from "next/link";
interface ILinkTextProps {
href: string;
children: React.ReactNode;
classNameChildren?: string;
target?: string;
}
/**
* This component had to be made in order to make i18n work with next/link
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
**/
export const LinkText = (props: ILinkTextProps) => {
const { target, href, children, classNameChildren, ...moreProps } = props;
return (
<Link href={href || ""} {...moreProps}>
<a target={target || ""} className={classNameChildren}>
{children}
</a>
</Link>
);
};
+15 -20
View File
@@ -9,26 +9,21 @@ const PoweredByCal = () => {
const isEmbed = useIsEmbed(); const isEmbed = useIsEmbed();
return ( return (
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}> <div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
<Link href={POWERED_BY_URL}> <Link
<a target="_blank" className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white"> href={POWERED_BY_URL}
{t("powered_by")}{" "} target="_blank"
{ className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
// eslint-disable-next-line @next/next/no-img-element {t("powered_by")}{" "}
<img <img
className="relative -mt-px inline h-[10px] w-auto dark:hidden" className="relative -mt-px inline h-[10px] w-auto dark:hidden"
src="/cal-logo-word.svg" src="/cal-logo-word.svg"
alt="Cal.com Logo" alt="Cal.com Logo"
/> />
} <img
{ className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
// eslint-disable-next-line @next/next/no-img-element src="/cal-logo-word-dark.svg"
<img alt="Cal.com Logo"
className="relativ -mt-px hidden h-[10px] w-auto dark:inline" />
src="/cal-logo-word-dark.svg"
alt="Cal.com Logo"
/>
}
</a>
</Link> </Link>
</div> </div>
); );
@@ -13,8 +13,8 @@ export default function SettingInputContainer({
<div className="space-y-3"> <div className="space-y-3">
<div className="block sm:flex"> <div className="block sm:flex">
<div className="min-w-48 mb-4 sm:mb-0"> <div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-neutral-700"> <label htmlFor={htmlFor} className="mt-1 flex text-sm font-medium text-gray-700">
<Icon className="mt-0.5 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" /> <Icon className="mt-0.5 h-4 w-4 text-gray-500 ltr:mr-2 rtl:ml-2" />
{label} {label}
</label> </label>
</div> </div>
@@ -187,7 +187,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<div className="flex justify-items-center"> <div className="flex justify-items-center">
<Label htmlFor="username">{t("username")}</Label> <Label htmlFor="username">{t("username")}</Label>
</div> </div>
<div className="mt-2 flex rounded-md"> <div className="flex rounded-md">
<span <span
className={classNames( className={classNames(
isInputUsernamePremium ? "border-1 border-orange-400 " : "", isInputUsernamePremium ? "border-1 border-orange-400 " : "",
@@ -118,7 +118,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
return ( return (
<div> <div>
<div className="mt-2 flex rounded-md"> <div className="flex rounded-md">
<div className="relative w-full"> <div className="relative w-full">
<TextField <TextField
ref={usernameRef} ref={usernameRef}
@@ -21,7 +21,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{React.createElement( {React.createElement(
descriptionAsLabel ? "div" : "label", descriptionAsLabel ? "div" : "label",
{ {
className: "flex text-sm font-medium text-neutral-700", className: "flex text-sm font-medium text-gray-700",
...(!descriptionAsLabel ...(!descriptionAsLabel
? { ? {
htmlFor: rest.id, htmlFor: rest.id,
@@ -39,7 +39,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
{ {
className: classNames( className: classNames(
"relative flex items-start", "relative flex items-start",
descriptionAsLabel ? "text-neutral-700" : "text-neutral-900" descriptionAsLabel ? "text-gray-700" : "text-gray-900"
), ),
}, },
<> <>
@@ -50,7 +50,7 @@ export const CheckedSelect = ({
{option.label} {option.label}
<Icon.FiX <Icon.FiX
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))} onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-neutral-500" className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500"
/> />
</div> </div>
))} ))}
+1 -1
View File
@@ -10,7 +10,7 @@ const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, re
<div className="block sm:flex"> <div className="block sm:flex">
{!!label && ( {!!label && (
<div className="min-w-48 mb-4 sm:mb-0"> <div className="min-w-48 mb-4 sm:mb-0">
<label htmlFor={rest.id} className="flex h-full items-center text-sm font-medium text-neutral-700"> <label htmlFor={rest.id} className="flex h-full items-center text-sm font-medium text-gray-700">
{label} {label}
</label> </label>
</div> </div>
@@ -1,79 +0,0 @@
import React, { ReactNode, useState } from "react";
import classNames from "@lib/classNames";
type RadioAreaProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
onChange?: (value: string) => void;
defaultChecked?: boolean;
};
const RadioArea = (props: RadioAreaProps) => {
return (
<label
className={classNames(
"border-1 block border p-4 focus:outline-none focus:ring focus:ring-neutral-500",
props.checked && "border-brand",
props.className
)}>
<input
onChange={(e) => {
if (typeof props.onChange === "function") {
props.onChange(e.target.value);
}
}}
checked={props.checked}
className="float-right ml-3 text-neutral-900 focus:ring-neutral-500"
name={props.name}
value={props.value}
type="radio"
/>
{props.children}
</label>
);
};
type ChildrenProps = {
props: RadioAreaProps;
children?: ReactNode;
};
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
children: ChildrenProps | ChildrenProps[];
name?: string;
onChange?: (value: string) => void;
}
const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
const [checkedIdx, setCheckedIdx] = useState<number | null>(null);
const changeHandler = (value: string, idx: number) => {
if (onChange) {
onChange(value);
}
setCheckedIdx(idx);
};
return (
<div {...passThroughProps}>
{(Array.isArray(children) ? children : [children]).map((child, idx: number) => {
if (checkedIdx === null && child.props.defaultChecked) {
setCheckedIdx(idx);
}
return (
<Item
{...child.props}
key={idx}
name={name}
checked={idx === checkedIdx}
onChange={(value: string) => changeHandler(value, idx)}>
{child.props.children}
</Item>
);
})}
</div>
);
};
const Item = RadioArea;
const Group = RadioAreaGroup;
export { RadioArea, RadioAreaGroup, Item, Group };
@@ -1,61 +0,0 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import React from "react";
import { Icon } from "@calcom/ui";
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import { RadioArea, RadioAreaGroup } from "@components/ui/form/radio-area/RadioAreaGroup";
interface OptionProps
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
description?: string;
}
interface RadioAreaSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
options: OptionProps[]; // allow options to be passed programmatically, like options={}
onChange?: (value: string) => void;
}
export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
const { t } = useLocale();
const {
options,
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
placeholder = t("select"),
} = props;
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
options.find((option: OptionProps) => option.value === value)?.label;
return (
<Collapsible className={classNames("w-full", props.className)}>
<CollapsibleTrigger
type="button"
disabled={disabled}
className={classNames(
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left text-sm",
disabled && "cursor-default bg-gray-200 focus:ring-0 "
)}>
{getLabel(props.value) ?? placeholder}
<Icon.FiChevronDown className="float-right h-5 w-5 text-neutral-500" />
</CollapsibleTrigger>
<CollapsibleContent>
<RadioAreaGroup className="space-y-2 text-sm" name={props.name} onChange={props.onChange}>
{options.map((option) => (
<RadioArea
{...option}
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}
defaultChecked={props.value === option.value}>
<strong className="mb-1 block">{option.label}</strong>
<p>{option.description}</p>
</RadioArea>
))}
</RadioAreaGroup>
</CollapsibleContent>
</Collapsible>
);
};
export default Select;
@@ -1,2 +0,0 @@
export * from "./RadioAreaGroup";
export * from "./Select";
+1
View File
@@ -1,3 +1,4 @@
// lets refactor and move this into packages/lib/hooks/
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
const useMediaQuery = (query: string) => { const useMediaQuery = (query: string) => {
+6
View File
@@ -89,6 +89,12 @@ const nextConfig = {
eslint: { eslint: {
ignoreDuringBuilds: !!process.env.CI, ignoreDuringBuilds: !!process.env.CI,
}, },
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
// modularizeImports: {
// "@calcom/ui": {
// transform: "@calcom/ui/components/{{member}}",
// },
// },
images: { images: {
unoptimized: true, unoptimized: true,
}, },
+5 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.4.4", "version": "2.5.0",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -67,6 +67,7 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"dotenv-cli": "^6.0.0", "dotenv-cli": "^6.0.0",
"eslint-config-next": "^13.1.1",
"googleapis": "^84.0.0", "googleapis": "^84.0.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
@@ -80,16 +81,14 @@
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"micro": "^9.4.1", "micro": "^9.4.1",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"next": "^12.3.1", "next": "^13.1.1",
"next-auth": "^4.10.3", "next-auth": "^4.10.3",
"next-axiom": "^0.10.0", "next-axiom": "^0.16.0",
"next-collect": "^0.2.1", "next-collect": "^0.2.1",
"next-i18next": "^11.3.0", "next-i18next": "^11.3.0",
"next-mdx-remote": "^4.1.0",
"next-seo": "^4.26.0", "next-seo": "^4.26.0",
"next-themes": "^0.2.0", "next-themes": "^0.2.0",
"next-transpile-modules": "^9.0.0", "next-transpile-modules": "^10.0.0",
"nock": "^13.2.8",
"nodemailer": "^6.7.8", "nodemailer": "^6.7.8",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
+46 -48
View File
@@ -98,26 +98,26 @@ export default function Custom404() {
<ul role="list" className="divide-y divide-gray-200 border-gray-200"> <ul role="list" className="divide-y divide-gray-200 border-gray-200">
<li className="px-4 py-2"> <li className="px-4 py-2">
<Link href={`${DEVELOPER_DOCS}/self-hosting/installation`}> <Link
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> href={`${DEVELOPER_DOCS}/self-hosting/installation`}
<div className="flex-shrink-0"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <div className="flex-shrink-0">
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" /> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
{t("prisma_studio_tip")}
</span> </span>
</div> </h3>
<div className="min-w-0 flex-1"> <p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
<h3 className="text-base font-medium text-gray-900"> </div>
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <div className="flex-shrink-0 self-center">
<span className="absolute inset-0" aria-hidden="true" /> <Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
{t("prisma_studio_tip")} </div>
</span>
</h3>
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
</div>
<div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
</a>
</Link> </Link>
</li> </li>
<li className="px-4 py-2"> <li className="px-4 py-2">
@@ -167,11 +167,11 @@ export default function Custom404() {
</li> </li>
</ul> </ul>
<div className="mt-8"> <div className="mt-8">
<Link href={`${WEBSITE_URL}/enterprise`}> <Link
<a className="text-base font-medium text-black hover:text-gray-500"> href={`${WEBSITE_URL}/enterprise`}
{t("contact_sales")} className="text-base font-medium text-black hover:text-gray-500">
<span aria-hidden="true"> &rarr;</span> {t("contact_sales")}
</a> <span aria-hidden="true"> &rarr;</span>
</Link> </Link>
</div> </div>
</div> </div>
@@ -243,26 +243,26 @@ export default function Custom404() {
<ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200"> <ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200">
{links.map((link, linkIdx) => ( {links.map((link, linkIdx) => (
<li key={linkIdx} className="px-4 py-2"> <li key={linkIdx} className="px-4 py-2">
<Link href={link.href}> <Link
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse"> href={link.href}
<div className="flex-shrink-0"> className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50"> <div className="flex-shrink-0">
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" /> <span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" />
</span>
</div>
<div className="min-w-0 flex-1">
<h3 className="text-base font-medium text-gray-900">
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
<span className="absolute inset-0" aria-hidden="true" />
{link.title}
</span> </span>
</div> </h3>
<div className="min-w-0 flex-1"> <p className="text-base text-gray-500">{link.description}</p>
<h3 className="text-base font-medium text-gray-900"> </div>
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2"> <div className="flex-shrink-0 self-center">
<span className="absolute inset-0" aria-hidden="true" /> <Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
{link.title} </div>
</span>
</h3>
<p className="text-base text-gray-500">{link.description}</p>
</div>
<div className="flex-shrink-0 self-center">
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
</a>
</Link> </Link>
</li> </li>
))} ))}
@@ -313,11 +313,9 @@ export default function Custom404() {
</li> </li>
</ul> </ul>
<div className="mt-8"> <div className="mt-8">
<Link href="/"> <Link href="/" className="text-base font-medium text-black hover:text-gray-500">
<a className="text-base font-medium text-black hover:text-gray-500"> {t("or_go_back_home")}
{t("or_go_back_home")} <span aria-hidden="true"> &rarr;</span>
<span aria-hidden="true"> &rarr;</span>
</a>
</Link> </Link>
</div> </div>
</div> </div>
+36 -40
View File
@@ -1,6 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -27,12 +26,11 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects"; import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Icon, HeadSeo } from "@calcom/ui"; import { Icon, HeadSeo, AvatarGroup } from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr"; import { EmbedProps } from "@lib/withEmbedSsr";
import AvatarGroup from "@components/ui/AvatarGroup";
import { AvatarSSR } from "@components/ui/AvatarSSR"; import { AvatarSSR } from "@components/ui/AvatarSSR";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@@ -60,25 +58,25 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
key={index} key={index}
className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600"> className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600">
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" /> <Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}> <Link
<a className="flex justify-between px-6 py-4" data-testid="event-type-link"> href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
<div className="flex-shrink"> className="flex justify-between px-6 py-4"
<p className="dark:text-darkgray-700 text-sm font-semibold text-neutral-900">{type.title}</p> data-testid="event-type-link">
<EventTypeDescription className="text-sm" eventType={type} /> <div className="flex-shrink">
</div> <p className="dark:text-darkgray-700 text-sm font-semibold text-gray-900">{type.title}</p>
<div className="mt-1 self-center"> <EventTypeDescription className="text-sm" eventType={type} />
<AvatarGroup </div>
border="border-2 border-white dark:border-darkgray-100" <div className="mt-1 self-center">
truncateAfter={4} <AvatarGroup
className="flex flex-shrink-0" truncateAfter={4}
size={10} className="flex flex-shrink-0"
items={props.users.map((user) => ({ size="sm"
alt: user.name || "", items={props.users.map((user) => ({
image: user.avatar || "", alt: user.name || "",
}))} image: user.avatar || "",
/> }))}
</div> />
</a> </div>
</Link> </Link>
</li> </li>
))} ))}
@@ -135,13 +133,13 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{isSingleUser && ( // When we deal with a single user, not dynamic group {isSingleUser && ( // When we deal with a single user, not dynamic group
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername} /> <AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername} />
<h1 className="font-cal mb-1 text-3xl text-neutral-900 dark:text-white"> <h1 className="font-cal mb-1 text-3xl text-gray-900 dark:text-white">
{nameOrUsername} {nameOrUsername}
{user.verified && ( {user.verified && (
<Icon.BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" /> <Icon.BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
)} )}
</h1> </h1>
<p className="dark:text-darkgray-600 text-s text-neutral-500">{user.bio}</p> <p className="dark:text-darkgray-600 text-s text-gray-500">{user.bio}</p>
</div> </div>
)} )}
<div <div
@@ -175,22 +173,20 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
href={{ href={{
pathname: `/${user.username}/${type.slug}`, pathname: `/${user.username}/${type.slug}`,
query, query,
}}> }}
<a onClick={async () => {
onClick={async () => { sdkActionManager?.fire("eventTypeSelected", {
sdkActionManager?.fire("eventTypeSelected", { eventType: type,
eventType: type, });
}); }}
}} className="block w-full p-5"
className="block w-full p-5" data-testid="event-type-link">
data-testid="event-type-link"> <div className="flex flex-wrap items-center">
<div className="flex flex-wrap items-center"> <h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700"> {type.title}
{type.title} </h2>
</h2> </div>
</div> <EventTypeDescription eventType={type} />
<EventTypeDescription eventType={type} />
</a>
</Link> </Link>
</div> </div>
)) ))
-1
View File
@@ -53,7 +53,6 @@ class MyDocument extends Document<Props> {
return ( return (
<Html lang={locale} dir={dir}> <Html lang={locale} dir={dir}>
<Head> <Head>
<link rel="preload" href="/cal.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
+6 -1
View File
@@ -1,10 +1,10 @@
import { IdentityProvider } from "@prisma/client"; import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/lib/auth";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { hashPassword } from "@lib/auth";
import slugify from "@lib/slugify"; import slugify from "@lib/slugify";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -12,6 +12,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return; return;
} }
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
res.status(403).json({ message: "Signup is disabled" });
return;
}
const data = req.body; const data = req.body;
const { email, password } = data; const { email, password } = data;
const username = slugify(data.username); const username = slugify(data.username);
@@ -37,6 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
location: true, location: true,
user: { user: {
select: { select: {
id: true,
email: true, email: true,
name: true, name: true,
username: true, username: true,
@@ -100,6 +101,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
startTime: booking.startTime.toISOString(), startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(), endTime: booking.endTime.toISOString(),
organizer: { organizer: {
id: user.id,
email: user.email, email: user.email,
name, name,
timeZone: user.timeZone, timeZone: user.timeZone,
+20 -2
View File
@@ -35,6 +35,24 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
uid: "xxyPr4cg2xx4XoS2KeMEQy", uid: "xxyPr4cg2xx4XoS2KeMEQy",
metadata: {}, metadata: {},
recurringEvent: null, recurringEvent: null,
appsStatus: [
{
appName: "Outlook Calendar",
type: "office365_calendar",
success: 1,
failures: 0,
errors: [],
warnings: [],
},
{
appName: "Google Meet",
type: "conferencing",
success: 0,
failures: 1,
errors: [],
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
},
],
}; };
req.statusCode = 200; req.statusCode = 200;
@@ -42,9 +60,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html"); res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate"); res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write( res.write(
renderEmail("OrganizerRequestEmail", { renderEmail("OrganizerScheduledEmail", {
attendee: evt.attendees[0],
calEvent: evt, calEvent: evt,
attendee: evt.organizer,
}) })
); );
res.end(); res.end();
+1 -1
View File
@@ -2,11 +2,11 @@ import { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth"; import type { Session } from "next-auth";
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath"; import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
import { getSession } from "@calcom/lib/auth";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType"; import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler"; import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
const defaultIntegrationAddHandler = async ({ const defaultIntegrationAddHandler = async ({
+13 -25
View File
@@ -1,34 +1,18 @@
import fs from "fs"; import fs from "fs";
import matter from "gray-matter"; import matter from "gray-matter";
import MarkdownIt from "markdown-it";
import { GetStaticPaths, GetStaticPropsContext } from "next"; import { GetStaticPaths, GetStaticPropsContext } from "next";
import { MDXRemote } from "next-mdx-remote";
import { serialize } from "next-mdx-remote/serialize";
import Image from "next/image";
import Link from "next/link";
import path from "path"; import path from "path";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry"; import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import App from "@components/apps/App"; import App from "@components/apps/App";
const components = { const md = new MarkdownIt("default", { html: true, breaks: true });
a: ({ href = "", ...otherProps }: JSX.IntrinsicElements["a"]) => (
<Link href={href}>
<a {...otherProps} />
</Link>
),
img: ({ src = "", alt = "", placeholder, ...rest }: JSX.IntrinsicElements["img"]) => (
<Image src={src} alt={alt} {...rest} />
),
// @TODO: In v2 the slider isn't shown anymore. However, to ensure the v1 pages keep
// working, this component is still rendered in the MDX content. To skip them in the v2
// content we have to render null here. In v2 the gallery is shown by directly
// using the `items` property from the MDX's meta data.
Slider: () => <></>,
};
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) { function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
return ( return (
@@ -50,10 +34,15 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
email={data.email} email={data.email}
licenseRequired={data.licenseRequired} licenseRequired={data.licenseRequired}
isProOnly={data.isProOnly} isProOnly={data.isProOnly}
images={source?.scope?.items as string[] | undefined} images={source.data?.items as string[] | undefined}
// tos="https://zoom.us/terms" // tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy" // privacy="https://zoom.us/privacy"
body={<MDXRemote {...source} components={components} />} body={
<>
{data.slug === "google-meet" && <ExisitingGoogleCal />}
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
</>
}
/> />
); );
} }
@@ -82,7 +71,7 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (!singleApp) return { notFound: true }; if (!singleApp) return { notFound: true };
const appDirname = app.dirName; const appDirname = app.dirName;
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/README.mdx`); const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
const postFilePath = path.join(README_PATH); const postFilePath = path.join(README_PATH);
let source = ""; let source = "";
@@ -90,16 +79,15 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
/* If the app doesn't have a README we fallback to the package description */ /* If the app doesn't have a README we fallback to the package description */
source = fs.readFileSync(postFilePath).toString(); source = fs.readFileSync(postFilePath).toString();
} catch (error) { } catch (error) {
console.log(`No README.mdx provided for: ${appDirname}`); console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
source = singleApp.description; source = singleApp.description;
} }
const { content, data } = matter(source); const { content, data } = matter(source);
const mdxSource = await serialize(content, { scope: data });
return { return {
props: { props: {
source: mdxSource, source: { content, data },
data: singleApp, data: singleApp,
}, },
}; };
@@ -4,9 +4,10 @@ import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { getAppRegistry } from "@calcom/app-store/_appRegistry"; import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { AppCard, Shell, SkeletonText } from "@calcom/ui"; import { AppCard, SkeletonText } from "@calcom/ui";
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) { export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
const { t, isLocaleReady } = useLocale(); const { t, isLocaleReady } = useLocale();
@@ -20,10 +21,10 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
backPath="/apps" backPath="/apps"
heading={ heading={
<> <>
<Link href="/apps"> <Link
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900"> href="/apps"
{isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "} className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
</a> {isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "}
</Link> </Link>
{category && ( {category && (
<span className="gap-1 text-gray-600"> <span className="gap-1 text-gray-600">
+19 -18
View File
@@ -2,8 +2,9 @@ import { InferGetStaticPropsType } from "next";
import Link from "next/link"; import Link from "next/link";
import { getAppRegistry } from "@calcom/app-store/_appRegistry"; import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, Shell, SkeletonText } from "@calcom/ui"; import { Icon, SkeletonText } from "@calcom/ui";
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) { export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
const { t, isLocaleReady } = useLocale(); const { t, isLocaleReady } = useLocale();
@@ -11,28 +12,28 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
return ( return (
<Shell isPublic large> <Shell isPublic large>
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0"> <div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
<Link href="/apps"> <Link
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900"> href="/apps"
<Icon.FiArrowLeft className="h-4 w-4" /> className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "} <Icon.FiArrowLeft className="h-4 w-4" />
</a> {isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
</Link> </Link>
</div> </div>
<div className="mb-16"> <div className="mb-16">
<div className="grid h-auto w-full grid-cols-5 gap-3"> <div className="grid h-auto w-full grid-cols-5 gap-3">
{categories.map((category) => ( {categories.map((category) => (
<Link key={category.name} href={"/apps/categories/" + category.name}> <Link
<a key={category.name}
data-testid={`app-store-category-${category.name}`} href={"/apps/categories/" + category.name}
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block"> data-testid={`app-store-category-${category.name}`}
<div className="self-center"> className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
<h3 className="font-medium capitalize">{category.name}</h3> <div className="self-center">
<p className="text-sm text-gray-500"> <h3 className="font-medium capitalize">{category.name}</h3>
{t("number_apps", { count: category.count })}{" "} <p className="text-sm text-gray-500">
<Icon.FiArrowRight className="inline-block h-4 w-4" /> {t("number_apps", { count: category.count })}{" "}
</p> <Icon.FiArrowRight className="inline-block h-4 w-4" />
</div> </p>
</a> </div>
</Link> </Link>
))} ))}
</div> </div>
+42 -13
View File
@@ -7,12 +7,31 @@ import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client"; import type { AppCategories } from "@calcom/prisma/client";
import { inferSSRProps } from "@calcom/types/inferSSRProps"; import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { AllApps, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui"; import {
AllApps,
AppStoreCategories,
HorizontalTabItemProps,
HorizontalTabs,
Icon,
TextField,
TrendingAppsSlider,
} from "@calcom/ui";
import AppsLayout from "@components/apps/layouts/AppsLayout"; import AppsLayout from "@components/apps/layouts/AppsLayout";
import { ssgInit } from "@server/lib/ssg"; import { ssgInit } from "@server/lib/ssg";
const tabs: HorizontalTabItemProps[] = [
{
name: "app_store",
href: "/apps",
},
{
name: "installed_apps",
href: "/apps/installed",
},
];
function AppsSearch({ function AppsSearch({
onChange, onChange,
className, className,
@@ -43,20 +62,30 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
heading={t("app_store")} heading={t("app_store")}
subtitle={t("app_store_description")} subtitle={t("app_store_description")}
actions={(className) => ( actions={(className) => (
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} /> <div className="flex w-full flex-col md:flex-row md:justify-between lg:w-auto">
<div className="lg:hidden">
<HorizontalTabs tabs={tabs} />
</div>
<div>
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
</div>
</div>
)} )}
headerClassName="sm:hidden lg:block hidden"
emptyStore={!appStore.length}> emptyStore={!appStore.length}>
{!searchText && ( <div className="flex flex-col gap-y-8">
<> {!searchText && (
<AppStoreCategories categories={categories} /> <>
<TrendingAppsSlider items={appStore} /> <AppStoreCategories categories={categories} />
</> <TrendingAppsSlider items={appStore} />
)} </>
<AllApps )}
apps={appStore} <AllApps
searchText={searchText} apps={appStore}
categories={categories.map((category) => category.name)} searchText={searchText}
/> categories={categories.map((category) => category.name)}
/>
</div>
</AppsLayout> </AppsLayout>
); );
} }
+22 -21
View File
@@ -15,8 +15,8 @@ import {
EmptyScreen, EmptyScreen,
Icon, Icon,
List, List,
ShellSubHeading,
AppSkeletonLoader as SkeletonLoader, AppSkeletonLoader as SkeletonLoader,
ShellSubHeading,
} from "@calcom/ui"; } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
@@ -92,8 +92,8 @@ function ConnectOrDisconnectIntegrationButton(props: {
} }
interface IntegrationsContainerProps { interface IntegrationsContainerProps {
variant?: keyof typeof InstalledAppVariants; variant?: typeof InstalledAppVariants[number];
exclude?: (keyof typeof InstalledAppVariants)[]; exclude?: typeof InstalledAppVariants[number][];
} }
interface IntegrationsListProps { interface IntegrationsListProps {
@@ -143,8 +143,10 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
automation: Icon.FiShare2, automation: Icon.FiShare2,
analytics: Icon.FiBarChart, analytics: Icon.FiBarChart,
payment: Icon.FiCreditCard, payment: Icon.FiCreditCard,
web3: Icon.FiBarChart,
other: Icon.FiGrid, other: Icon.FiGrid,
}; };
return ( return (
<QueryCell <QueryCell
query={query} query={query}
@@ -183,6 +185,7 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
buttonRaw={ buttonRaw={
<Button <Button
color="secondary" color="secondary"
data-testid={`connect-${variant || "other"}-apps`}
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}> href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
{t(`connect_${variant || "other"}_apps`)} {t(`connect_${variant || "other"}_apps`)}
</Button> </Button>
@@ -197,31 +200,29 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
}; };
const querySchema = z.object({ const querySchema = z.object({
category: z.nativeEnum(InstalledAppVariants), category: z.enum(InstalledAppVariants),
}); });
type querySchemaType = z.infer<typeof querySchema>;
export default function InstalledApps() { export default function InstalledApps() {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const category = router.query.category; const category = router.query.category as querySchemaType["category"];
const categoryList: querySchemaType["category"][] = [
"payment",
"conferencing",
"automation",
"analytics",
"web3",
];
return ( return (
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}> <InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
{(category === InstalledAppVariants.payment || category === InstalledAppVariants.conferencing) && ( {categoryList.includes(category) && <IntegrationsContainer variant={category} />}
<IntegrationsContainer variant={category} /> {category === "calendar" && <CalendarListContainer />}
)} {category === "other" && (
{(category === InstalledAppVariants.automation || category === InstalledAppVariants.analytics) && ( <IntegrationsContainer variant={category} exclude={[...categoryList, "calendar"]} />
<IntegrationsContainer variant={category} />
)}
{category === InstalledAppVariants.calendar && <CalendarListContainer />}
{category === InstalledAppVariants.other && (
<IntegrationsContainer
exclude={[
InstalledAppVariants.conferencing,
InstalledAppVariants.calendar,
InstalledAppVariants.analytics,
InstalledAppVariants.automation,
]}
/>
)} )}
</InstalledAppsLayout> </InstalledAppsLayout>
); );
+1 -1
View File
@@ -40,7 +40,7 @@ export default function Error() {
</div> </div>
</div> </div>
<div className="mt-5 sm:mt-6"> <div className="mt-5 sm:mt-6">
<Link href="/auth/login" passHref> <Link href="/auth/login" passHref legacyBehavior>
<Button className="flex w-full justify-center">{t("go_back_login")}</Button> <Button className="flex w-full justify-center">{t("go_back_login")}</Button>
</Link> </Link>
</div> </div>
+1 -1
View File
@@ -81,7 +81,7 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
<h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2> <h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2>
</div> </div>
<p>{t("request_is_expired_instructions")}</p> <p>{t("request_is_expired_instructions")}</p>
<Link href="/auth/forgot-password" passHref> <Link href="/auth/forgot-password" passHref legacyBehavior>
<button <button
type="button" type="button"
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"> className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
@@ -94,8 +94,8 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
footerText={ footerText={
!success && ( !success && (
<> <>
<Link href="/auth/login"> <Link href="/auth/login" className="font-medium text-gray-900">
<a className="font-medium text-neutral-900">{t("back_to_signin")}</a> {t("back_to_signin")}
</Link> </Link>
</> </>
) )
+12 -5
View File
@@ -115,7 +115,13 @@ export default function Login({
description={t("login")} description={t("login")}
showLogo showLogo
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")} heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}> footerText={
twoFactorRequired
? TwoFactorFooter
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
? LoginFooter
: null
}>
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form"> <form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form">
<div> <div>
@@ -133,10 +139,11 @@ export default function Login({
/> />
<div className="relative"> <div className="relative">
<div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0"> <div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0">
<Link href="/auth/forgot-password"> <Link
<a tabIndex={-1} className="text-sm font-medium text-gray-600"> href="/auth/forgot-password"
{t("forgot")} tabIndex={-1}
</a> className="text-sm font-medium text-gray-600">
{t("forgot")}
</Link> </Link>
</div> </div>
<PasswordField <PasswordField
+1 -1
View File
@@ -27,7 +27,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
{ {
title: t("enable_apps"), title: t("enable_apps"),
description: t("enable_apps_description"), description: t("enable_apps_description"),
content: <AdminAppsList baseURL="/auth/setup?step=2" />, content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
isLoading: false, isLoading: false,
}, },
]; ];
+2 -2
View File
@@ -4,6 +4,7 @@ import { z } from "zod";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules"; import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule"; import Schedule from "@calcom/features/schedules/components/Schedule";
import Shell from "@calcom/features/shell/Shell";
import { availabilityAsString } from "@calcom/lib/availability"; import { availabilityAsString } from "@calcom/lib/availability";
import { yyyymmdd } from "@calcom/lib/date-fns"; import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -16,7 +17,6 @@ import {
Form, Form,
Icon, Icon,
Label, Label,
Shell,
showToast, showToast,
Skeleton, Skeleton,
SkeletonText, SkeletonText,
@@ -60,7 +60,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
</span> </span>
</Tooltip> </Tooltip>
</h3> </h3>
<p className="mb-4 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p> <p className="mb-4 text-sm text-gray-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
<div className="mt-1 space-y-2"> <div className="mt-1 space-y-2">
<DateOverrideList <DateOverrideList
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))} excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
+2 -1
View File
@@ -2,9 +2,10 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"; import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { EmptyScreen, Icon, Shell, showToast } from "@calcom/ui"; import { EmptyScreen, Icon, showToast } from "@calcom/ui";
import { withQuery } from "@lib/QueryCell"; import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
+5 -6
View File
@@ -1,7 +1,8 @@
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Shell, SkeletonText } from "@calcom/ui"; import { SkeletonText } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery"; import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -55,7 +56,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
if (e.target.value) setSelectedDate(e.target.value); if (e.target.value) setSelectedDate(e.target.value);
}} }}
/> />
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small> <small className="block text-gray-400">{t("hover_over_bold_times_tip")}</small>
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md"> <div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6"> <div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
@@ -81,13 +82,11 @@ const AvailabilityView = ({ user }: { user: User }) => {
data-testid="troubleshooter-busy-time"> data-testid="troubleshooter-busy-time">
<div className="px-4 py-5 text-black sm:p-6"> <div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "} {t("calendar_shows_busy_between")}{" "}
<span <span className="font-medium text-gray-800" title={dayjs(slot.start).format("HH:mm")}>
className="font-medium text-neutral-800"
title={dayjs(slot.start).format("HH:mm")}>
{dayjs(slot.start).format("HH:mm")} {dayjs(slot.start).format("HH:mm")}
</span>{" "} </span>{" "}
{t("and")}{" "} {t("and")}{" "}
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}> <span className="font-medium text-gray-800" title={dayjs(slot.end).format("HH:mm")}>
{dayjs(slot.end).format("HH:mm")} {dayjs(slot.end).format("HH:mm")}
</span>{" "} </span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "} {t("on")} {dayjs(slot.start).format("D")}{" "}
+94 -94
View File
@@ -32,7 +32,7 @@ import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat"; import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage"; import { localStorage } from "@calcom/lib/webstorage";
import prisma, { baseUserSelect } from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client"; import { Prisma } from "@calcom/prisma/client";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, Icon, HeadSeo } from "@calcom/ui"; import { Button, EmailInput, Icon, HeadSeo } from "@calcom/ui";
@@ -322,7 +322,7 @@ export default function Success(props: SuccessProps) {
} }
return t("emailed_you_and_attendees" + titleSuffix); return t("emailed_you_and_attendees" + titleSuffix);
} }
const userIsOwner = !!(session?.user?.id && eventType.users.find((user) => (user.id = session.user.id))); const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
useTheme(isSuccessBookingPage ? props.profile.theme : "light"); useTheme(isSuccessBookingPage ? props.profile.theme : "light");
const title = t( const title = t(
`booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}` `booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
@@ -353,10 +353,10 @@ export default function Success(props: SuccessProps) {
)} )}
{userIsOwner && !isEmbed && ( {userIsOwner && !isEmbed && (
<div className="mt-2 ml-4 -mb-4"> <div className="mt-2 ml-4 -mb-4">
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}> <Link
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-transparent dark:hover:text-white"> href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
<Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")} className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-transparent dark:hover:text-white">
</a> <Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
</Link> </Link>
</div> </div>
)} )}
@@ -379,7 +379,7 @@ export default function Success(props: SuccessProps) {
<div <div
className={classNames( className={classNames(
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl", "main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border-neutral-700", isBackgroundTransparent ? "" : "dark:bg-darkgray-100 dark:border-darkgray-200 bg-white",
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle" "px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
)} )}
role="dialog" role="dialog"
@@ -408,7 +408,7 @@ export default function Success(props: SuccessProps) {
</div> </div>
<div className="mt-6 mb-8 text-center last:mb-0"> <div className="mt-6 mb-8 text-center last:mb-0">
<h3 <h3
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white" className="text-2xl font-semibold leading-6 text-gray-900 dark:text-white"
data-testid={isCancelled ? "cancelled-headline" : ""} data-testid={isCancelled ? "cancelled-headline" : ""}
id="modal-headline"> id="modal-headline">
{needsConfirmation && !isCancelled {needsConfirmation && !isCancelled
@@ -422,9 +422,10 @@ export default function Success(props: SuccessProps) {
: t("meeting_is_scheduled")} : t("meeting_is_scheduled")}
</h3> </h3>
<div className="mt-3"> <div className="mt-3">
<p className="text-neutral-600 dark:text-gray-300">{getTitle()}</p> <p className="text-gray-600 dark:text-gray-300">{getTitle()}</p>
</div> </div>
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-200 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
{(isCancelled || reschedule) && cancellationReason && ( {(isCancelled || reschedule) && cancellationReason && (
<> <>
<div className="font-medium"> <div className="font-medium">
@@ -499,7 +500,7 @@ export default function Success(props: SuccessProps) {
<> <>
<div className="mt-9 font-medium">{t("additional_notes")}</div> <div className="mt-9 font-medium">{t("additional_notes")}</div>
<div className="col-span-2 mb-2 mt-9"> <div className="col-span-2 mb-2 mt-9">
<p>{bookingInfo.description}</p> <p className="break-words">{bookingInfo.description}</p>
</div> </div>
</> </>
)} )}
@@ -573,7 +574,9 @@ export default function Success(props: SuccessProps) {
{!props.recurringBookings && ( {!props.recurringBookings && (
<span className="text-bookinglight inline text-gray-700"> <span className="text-bookinglight inline text-gray-700">
<span className="underline"> <span className="underline">
<Link href={`/reschedule/${bookingInfo?.uid}`}>{t("reschedule")}</Link> <Link href={`/reschedule/${bookingInfo?.uid}`} legacyBehavior>
{t("reschedule")}
</Link>
</span> </span>
<span className="mx-2">{t("or_lowercase")}</span> <span className="mx-2">{t("or_lowercase")}</span>
</span> </span>
@@ -592,7 +595,7 @@ export default function Success(props: SuccessProps) {
</> </>
) : ( ) : (
<> <>
<hr className="border-bookinglightest dark:border-darkgray-300" /> <hr className="border-bookinglightest dark:border-darkgray-200" />
<CancelBooking <CancelBooking
booking={{ uid: bookingInfo?.uid, title: bookingInfo?.title, id: bookingInfo?.id }} booking={{ uid: bookingInfo?.uid, title: bookingInfo?.title, id: bookingInfo?.id }}
profile={{ name: props.profile.name, slug: props.profile.slug }} profile={{ name: props.profile.name, slug: props.profile.slug }}
@@ -633,17 +636,16 @@ export default function Success(props: SuccessProps) {
? "&recur=" + ? "&recur=" +
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString()) encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
: "") : "")
}> }
<a className="h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-neutral-700 dark:text-white"> className="h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-neutral-700 dark:text-white">
<svg <svg
className="-mt-1.5 inline-block h-4 w-4" className="-mt-1.5 inline-block h-4 w-4"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
<title>Google</title> <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" /> <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> </svg>
</a>
</Link> </Link>
<Link <Link
href={ href={
@@ -657,19 +659,17 @@ export default function Success(props: SuccessProps) {
"&subject=" + "&subject=" +
eventName eventName
) + (location ? "&location=" + location : "") ) + (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"
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">
target="_blank"> <svg
<svg className="mr-1 -mt-1.5 inline-block h-4 w-4"
className="mr-1 -mt-1.5 inline-block h-4 w-4" fill="currentColor"
fill="currentColor" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24"> <title>Microsoft Outlook</title>
<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" />
<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>
</svg>
</a>
</Link> </Link>
<Link <Link
href={ href={
@@ -683,34 +683,31 @@ export default function Success(props: SuccessProps) {
"&subject=" + "&subject=" +
eventName eventName
) + (location ? "&location=" + location : "") ) + (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"
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">
target="_blank"> <svg
<svg className="mr-1 -mt-1.5 inline-block h-4 w-4"
className="mr-1 -mt-1.5 inline-block h-4 w-4" fill="currentColor"
fill="currentColor" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24"> <title>Microsoft Office</title>
<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" />
<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>
</svg>
</a>
</Link> </Link>
<Link href={"data:text/calendar," + eventLink()}> <Link
<a href={"data:text/calendar," + eventLink()}
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white" 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"}> download={props.eventType.title + ".ics"}>
<svg <svg
version="1.1" version="1.1"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000" viewBox="0 0 1000 1000"
className="mr-1 -mt-1.5 inline-block h-4 w-4"> className="mr-1 -mt-1.5 inline-block h-4 w-4">
<title>{t("other")}</title> <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" /> <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> </svg>
</a>
</Link> </Link>
</div> </div>
</div> </div>
@@ -849,6 +846,17 @@ export function RecurringBookings({
} }
const getEventTypesFromDB = async (id: number) => { const getEventTypesFromDB = async (id: number) => {
const userSelect = {
id: true,
name: true,
username: true,
hideBranding: true,
theme: true,
brandColor: true,
darkBrandColor: true,
email: true,
timeZone: true,
};
const eventType = await prisma.eventType.findUnique({ const eventType = await prisma.eventType.findUnique({
where: { where: {
id, id,
@@ -867,17 +875,17 @@ const getEventTypesFromDB = async (id: number) => {
locations: true, locations: true,
price: true, price: true,
currency: true, currency: true,
owner: {
select: userSelect,
},
users: { users: {
select: userSelect,
},
hosts: {
select: { select: {
id: true, user: {
name: true, select: userSelect,
username: true, },
hideBranding: true,
theme: true,
brandColor: true,
darkBrandColor: true,
email: true,
timeZone: true,
}, },
}, },
team: { team: {
@@ -921,6 +929,7 @@ const schema = z.object({
uid: z.string(), uid: z.string(),
email: z.string().optional(), email: z.string().optional(),
eventTypeSlug: z.string().optional(), eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
}); });
const handleSeatsEventTypeOnBooking = ( const handleSeatsEventTypeOnBooking = (
@@ -954,7 +963,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context); const ssr = await ssrInit(context);
const parsedQuery = schema.safeParse(context.query); const parsedQuery = schema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true }; if (!parsedQuery.success) return { notFound: true };
const { uid, email, eventTypeSlug } = parsedQuery.data; const { uid, email, eventTypeSlug, cancel } = parsedQuery.data;
const bookingInfo = await prisma.booking.findFirst({ const bookingInfo = await prisma.booking.findFirst({
where: { where: {
@@ -1017,27 +1026,18 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}; };
} }
if (!eventTypeRaw.users.length && eventTypeRaw.userId) { eventTypeRaw.users = !!eventTypeRaw.hosts?.length
// TODO we should add `user User` relation on `EventType` so this extra query isn't needed ? eventTypeRaw.hosts.map((host) => host.user)
const user = await prisma.user.findUnique({ : eventTypeRaw.users;
where: {
id: eventTypeRaw.userId,
},
select: baseUserSelect,
});
if (user) {
eventTypeRaw.users.push({
...user,
avatar: "",
allowDynamicBooking: true,
});
}
}
if (!eventTypeRaw.users.length) { if (!eventTypeRaw.users.length) {
return { if (!eventTypeRaw.owner)
notFound: true, return {
}; notFound: true,
};
eventTypeRaw.users.push({
...eventTypeRaw.owner,
});
} }
const eventType = { const eventType = {
@@ -1049,7 +1049,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs), customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
}; };
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl) { if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) {
return { return {
redirect: { redirect: {
destination: eventType.successRedirectUrl, destination: eventType.successRedirectUrl,
+26 -11
View File
@@ -3,7 +3,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { PeriodType, SchedulingType } from "@prisma/client"; import { PeriodType, SchedulingType } from "@prisma/client";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -15,6 +14,7 @@ import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType"; import findDurationType from "@calcom/lib/findDurationType";
import getEventTypeById from "@calcom/lib/getEventTypeById"; import getEventTypeById from "@calcom/lib/getEventTypeById";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@@ -64,7 +64,6 @@ export type FormValues = {
phone?: string; phone?: string;
}[]; }[];
customInputs: CustomInputParsed[]; customInputs: CustomInputParsed[];
users: string[];
schedule: number; schedule: number;
periodType: PeriodType; periodType: PeriodType;
periodDays: number; periodDays: number;
@@ -85,6 +84,8 @@ export type FormValues = {
}; };
successRedirectUrl: string; successRedirectUrl: string;
bookingLimits?: BookingLimit; bookingLimits?: BookingLimit;
hosts: { userId: number }[];
hostsFixed: { userId: number }[];
}; };
export type CustomInputParsed = typeof customInputSchema._output; export type CustomInputParsed = typeof customInputSchema._output;
@@ -111,15 +112,15 @@ export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
const EventTypePage = (props: EventTypeSetupProps) => { const EventTypePage = (props: EventTypeSetupProps) => {
const { t } = useLocale(); const { t } = useLocale();
const utils = trpc.useContext(); const utils = trpc.useContext();
const router = useRouter(); const {
const { tabName } = querySchema.parse(router.query); data: { tabName },
} = useTypedQuery(querySchema);
const { data: eventTypeApps } = trpc.viewer.apps.useQuery({ const { data: eventTypeApps } = trpc.viewer.apps.useQuery({
extendsFeature: "EventType", extendsFeature: "EventType",
}); });
const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props; const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props;
const [animationParentRef] = useAutoAnimate<HTMLDivElement>(); const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
const updateMutation = trpc.viewer.eventTypes.update.useMutation({ const updateMutation = trpc.viewer.eventTypes.update.useMutation({
@@ -185,6 +186,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
description: eventType.description ?? undefined, description: eventType.description ?? undefined,
schedule: eventType.schedule || undefined, schedule: eventType.schedule || undefined,
bookingLimits: eventType.bookingLimits || undefined, bookingLimits: eventType.bookingLimits || undefined,
length: eventType.length,
hidden: eventType.hidden, hidden: eventType.hidden,
periodDates: { periodDates: {
startDate: periodDates.startDate, startDate: periodDates.startDate,
@@ -194,12 +196,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0", periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
schedulingType: eventType.schedulingType, schedulingType: eventType.schedulingType,
minimumBookingNotice: eventType.minimumBookingNotice, minimumBookingNotice: eventType.minimumBookingNotice,
minimumBookingNoticeInDurationType: convertToNewDurationType(
"minutes",
findDurationType(eventType.minimumBookingNotice),
eventType.minimumBookingNotice
),
metadata, metadata,
hosts: !!eventType.hosts?.length
? eventType.hosts.filter((host) => !host.isFixed)
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
.map((user) => ({ userId: user.id })),
hostsFixed: !!eventType.hosts?.length
? eventType.hosts.filter((host) => host.isFixed)
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
.map((user) => ({ userId: user.id })),
}, },
resolver: zodResolver( resolver: zodResolver(
z z
@@ -237,7 +244,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
/> />
), ),
availability: <AvailabilityTab isTeamEvent={!!team} />, availability: <AvailabilityTab isTeamEvent={!!team} />,
team: <EventTeamTab eventType={eventType} teamMembers={teamMembers} team={team} />, team: <EventTeamTab teamMembers={teamMembers} team={team} />,
limits: <EventLimitsTab eventType={eventType} />, limits: <EventLimitsTab eventType={eventType} />,
advanced: <EventAdvancedTab eventType={eventType} team={team} />, advanced: <EventAdvancedTab eventType={eventType} team={team} />,
recurring: <EventRecurringTab eventType={eventType} />, recurring: <EventRecurringTab eventType={eventType} />,
@@ -278,6 +285,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
locations, locations,
metadata, metadata,
customInputs, customInputs,
hosts: hostsInput,
hostsFixed,
// We don't need to send send these values to the backend // We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled, seatsPerTimeSlotEnabled,
@@ -286,6 +295,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
...input ...input
} = values; } = values;
const hosts: (typeof hostsInput[number] & { isFixed?: boolean })[] = [];
if (hostsInput || hostsFixed) {
hosts.push(...hostsInput.concat(hostsFixed.map((host) => ({ isFixed: true, ...host }))));
}
if (bookingLimits) { if (bookingLimits) {
const isValid = validateBookingLimitOrder(bookingLimits); const isValid = validateBookingLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error")); if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@@ -303,6 +317,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
updateMutation.mutate({ updateMutation.mutate({
...input, ...input,
hosts,
locations, locations,
recurringEvent, recurringEvent,
periodStartDate: periodDates.startDate, periodStartDate: periodDates.startDate,
+64 -74
View File
@@ -8,6 +8,7 @@ import {
CreateEventTypeButton, CreateEventTypeButton,
EventTypeDescriptionLazy as EventTypeDescription, EventTypeDescriptionLazy as EventTypeDescription,
} from "@calcom/features/eventtypes/components"; } from "@calcom/features/eventtypes/components";
import Shell from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react"; import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
@@ -26,9 +27,10 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
EmptyScreen, EmptyScreen,
Icon, Icon,
Shell,
showToast, showToast,
Switch, Switch,
Avatar,
AvatarGroup,
Tooltip, Tooltip,
} from "@calcom/ui"; } from "@calcom/ui";
@@ -37,8 +39,6 @@ import { HttpError } from "@lib/core/http/error";
import { EmbedButton, EmbedDialog } from "@components/Embed"; import { EmbedButton, EmbedDialog } from "@components/Embed";
import SkeletonLoader from "@components/eventtype/SkeletonLoader"; import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@@ -64,27 +64,26 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<Link href={`/event-types/${type.id}?tabName=setup`}> <Link
<a href={`/event-types/${type.id}?tabName=setup`}
className="flex-1 overflow-hidden pr-4 text-sm" className="flex-1 overflow-hidden pr-4 text-sm"
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}> title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
<div> <div>
<span <span
className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1" className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1"
data-testid={"event-type-title-" + type.id}> data-testid={"event-type-title-" + type.id}>
{type.title} {type.title}
</span>
<small
className="hidden font-normal leading-4 text-gray-600 sm:inline"
data-testid={"event-type-slug-" + type.id}>{`/${group.profile.slug}/${type.slug}`}</small>
{readOnly && (
<span className="rtl:ml-2inline items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2 ltr:mr-2">
{t("readonly")}
</span> </span>
<small )}
className="hidden font-normal leading-4 text-gray-600 sm:inline" </div>
data-testid={"event-type-slug-" + type.id}>{`/${group.profile.slug}/${type.slug}`}</small> <EventTypeDescription eventType={type} />
{readOnly && (
<span className="rtl:ml-2inline items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2 ltr:mr-2">
{t("readonly") as string}
</span>
)}
</div>
<EventTypeDescription eventType={type} />
</a>
</Link> </Link>
); );
}; };
@@ -280,9 +279,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="flex justify-between space-x-2 rtl:space-x-reverse"> <div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.users?.length > 1 && ( {type.users?.length > 1 && (
<AvatarGroup <AvatarGroup
border="border-2 border-white"
className="relative top-1 right-3" className="relative top-1 right-3"
size={8} size="sm"
truncateAfter={4} truncateAfter={4}
items={type.users.map((organizer) => ({ items={type.users.map((organizer) => ({
alt: organizer.name || "", alt: organizer.name || "",
@@ -297,7 +295,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("hidden")} {t("hidden")}
</Badge> </Badge>
)} )}
<Tooltip content={t("show_eventtype_on_profile") as string}> <Tooltip content={t("show_eventtype_on_profile")}>
<div className="self-center rounded-md p-2 hover:bg-gray-200"> <div className="self-center rounded-md p-2 hover:bg-gray-200">
<Switch <Switch
name="Hidden" name="Hidden"
@@ -310,7 +308,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</Tooltip> </Tooltip>
<ButtonGroup combined> <ButtonGroup combined>
<Tooltip content={t("preview") as string}> <Tooltip content={t("preview")}>
<Button <Button
color="secondary" color="secondary"
target="_blank" target="_blank"
@@ -320,7 +318,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
/> />
</Tooltip> </Tooltip>
<Tooltip content={t("copy_link") as string}> <Tooltip content={t("copy_link")}>
<Button <Button
color="secondary" color="secondary"
size="icon" size="icon"
@@ -350,7 +348,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
data-testid={"event-type-edit-" + type.id} data-testid={"event-type-edit-" + type.id}
StartIcon={Icon.FiEdit2} StartIcon={Icon.FiEdit2}
onClick={() => router.push("/event-types/" + type.id)}> onClick={() => router.push("/event-types/" + type.id)}>
{t("edit") as string} {t("edit")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
@@ -359,7 +357,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiCopy} StartIcon={Icon.FiCopy}
onClick={() => openDuplicateModal(type)}> onClick={() => openDuplicateModal(type)}>
{t("duplicate") as string} {t("duplicate")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
@@ -372,18 +370,19 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("embed")} {t("embed")}
</EmbedButton> </EmbedButton>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" /> <DropdownMenuSeparator />
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */} {/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && ( {(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
<DropdownMenuItem> <DropdownMenuItem>
<DropdownItem <DropdownItem
color="destructive"
onClick={() => { onClick={() => {
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id); setDeleteDialogTypeId(type.id);
}} }}
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
className="w-full rounded-none"> className="w-full rounded-none">
{t("delete") as string} {t("delete")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
)} )}
@@ -402,15 +401,13 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
<Link href={calLink}> <Link href={calLink} target="_blank">
<a target="_blank"> <Button
<Button color="minimal"
color="minimal" StartIcon={Icon.FiExternalLink}
StartIcon={Icon.FiExternalLink} className="w-full rounded-none">
className="w-full rounded-none"> {t("preview")}
{t("preview") as string} </Button>
</Button>
</a>
</Link> </Link>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
@@ -424,7 +421,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
navigator.clipboard.writeText(calLink); navigator.clipboard.writeText(calLink);
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
}}> }}>
{t("copy_link") as string} {t("copy_link")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
{isNativeShare ? ( {isNativeShare ? (
@@ -445,7 +442,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
.then(() => showToast(t("link_shared"), "success")) .then(() => showToast(t("link_shared"), "success"))
.catch(() => showToast(t("failed"), "error")); .catch(() => showToast(t("failed"), "error"));
}}> }}>
{t("share") as string} {t("share")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
) : null} ) : null}
@@ -456,7 +453,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
color="minimal" color="minimal"
className="w-full rounded-none" className="w-full rounded-none"
StartIcon={Icon.FiEdit}> StartIcon={Icon.FiEdit}>
{t("edit") as string} {t("edit")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
@@ -467,10 +464,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
data-testid={"event-type-duplicate-" + type.id} data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiCopy} StartIcon={Icon.FiCopy}
onClick={() => openDuplicateModal(type)}> onClick={() => openDuplicateModal(type)}>
{t("duplicate") as string} {t("duplicate")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" /> <DropdownMenuSeparator />
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
<Button <Button
onClick={() => { onClick={() => {
@@ -480,7 +477,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
color="destructive" color="destructive"
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
className="w-full rounded-none"> className="w-full rounded-none">
{t("delete") as string} {t("delete")}
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@@ -502,7 +499,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
e.preventDefault(); e.preventDefault();
deleteEventTypeHandler(deleteDialogTypeId); deleteEventTypeHandler(deleteDialogTypeId);
}}> }}>
{t("delete_event_type_description") as string} {t("delete_event_type_description")}
</ConfirmationDialogContent> </ConfirmationDialogContent>
</Dialog> </Dialog>
</div> </div>
@@ -515,38 +512,34 @@ const EventTypeListHeading = ({
teamId, teamId,
}: EventTypeListHeadingProps): JSX.Element => { }: EventTypeListHeadingProps): JSX.Element => {
return ( return (
<div className="mb-4 flex"> <div className="mb-4 flex items-center space-x-2">
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}> <Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
<a> <Avatar
<Avatar alt={profile?.name || ""}
alt={profile?.name || ""} imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined} size="sm"
size={8} className="mt-1 inline ltr:mr-2 rtl:ml-2"
className="mt-1 inline ltr:mr-2 rtl:ml-2" />
/>
</a>
</Link> </Link>
<div> <div>
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}> <Link
<a className="font-bold">{profile?.name || ""}</a> href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
className="font-bold">
{profile?.name || ""}
</Link> </Link>
{membershipCount && teamId && ( {membershipCount && teamId && (
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 ltr:mr-2 rtl:ml-2"> <span className="relative -top-px text-xs text-gray-500 ltr:ml-2 ltr:mr-2 rtl:ml-2">
<Link href={`/settings/teams/${teamId}/members`}> <Link href={`/settings/teams/${teamId}/members`}>
<a> <Badge variant="gray">
<Badge variant="gray"> <Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" /> {membershipCount}
{membershipCount} </Badge>
</Badge>
</a>
</Link> </Link>
</span> </span>
)} )}
{profile?.slug && ( {profile?.slug && (
<Link href={`${CAL_URL}/${profile.slug}`}> <Link href={`${CAL_URL}/${profile.slug}`} className="block text-xs text-gray-500">
<a className="block text-xs text-neutral-500">{`${CAL_URL?.replace("https://", "")}/${ {`${CAL_URL?.replace("https://", "")}/${profile.slug}`}
profile.slug
}`}</a>
</Link> </Link>
)} )}
</div> </div>
@@ -581,10 +574,7 @@ const EventTypesPage = () => {
return ( return (
<div> <div>
<Shell <Shell heading={t("event_types_page_title")} subtitle={t("event_types_page_subtitle")} CTA={<CTA />}>
heading={t("event_types_page_title") as string}
subtitle={t("event_types_page_subtitle") as string}
CTA={<CTA />}>
<WithQuery <WithQuery
customLoader={<SkeletonLoader />} customLoader={<SkeletonLoader />}
success={({ data }) => ( success={({ data }) => (
@@ -178,6 +178,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
throw new Error("User from session not found"); throw new Error("User from session not found");
} }
if (user.completedOnboarding) {
return { redirect: { permanent: false, destination: "/event-types" } };
}
return { return {
props: { props: {
...(await serverSideTranslations(context.locale ?? "", ["common"])), ...(await serverSideTranslations(context.locale ?? "", ["common"])),
+1 -1
View File
@@ -1,5 +1,5 @@
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MobileNavigationMoreItems, Shell } from "@calcom/ui";
export default function MorePage() { export default function MorePage() {
const { t } = useLocale(); const { t } = useLocale();
@@ -7,7 +7,6 @@ import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { import {
Badge,
Button, Button,
ColorPicker, ColorPicker,
Form, Form,
@@ -17,6 +16,7 @@ import {
SkeletonContainer, SkeletonContainer,
SkeletonText, SkeletonText,
Switch, Switch,
UpgradeTeamsBadge,
} from "@calcom/ui"; } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
@@ -49,6 +49,7 @@ const AppearanceView = () => {
const session = useSession(); const session = useSession();
const utils = trpc.useContext(); const utils = trpc.useContext();
const { data: user, isLoading } = trpc.viewer.me.useQuery(); const { data: user, isLoading } = trpc.viewer.me.useQuery();
const { data: dataHasTeamPlan, isLoading: isLoadingHasTeamPlan } = trpc.viewer.teams.hasTeamPlan.useQuery();
const formMethods = useForm({ const formMethods = useForm({
defaultValues: { defaultValues: {
@@ -73,7 +74,8 @@ const AppearanceView = () => {
}, },
}); });
if (isLoading) return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />; if (isLoading || isLoadingHasTeamPlan)
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
if (!user) return null; if (!user) return null;
@@ -180,18 +182,18 @@ const AppearanceView = () => {
<p className="font-semibold ltr:mr-2 rtl:ml-2"> <p className="font-semibold ltr:mr-2 rtl:ml-2">
{t("disable_cal_branding", { appName: APP_NAME })} {t("disable_cal_branding", { appName: APP_NAME })}
</p> </p>
<Badge variant="gray">{t("pro")}</Badge> {!dataHasTeamPlan?.hasTeamPlan && <UpgradeTeamsBadge />}
</div> </div>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p> <p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p>
</div> </div>
<div className="flex-none"> <div className="flex-none">
<Switch <Switch
id="hideBranding" id="hideBranding"
disabled={!session.data?.user.belongsToActiveTeam} disabled={!dataHasTeamPlan?.hasTeamPlan}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
formMethods.setValue("hideBranding", checked, { shouldDirty: true }) formMethods.setValue("hideBranding", checked, { shouldDirty: true })
} }
checked={!session.data?.user.belongsToActiveTeam ? false : value} checked={!dataHasTeamPlan?.hasTeamPlan ? false : value}
/> />
</div> </div>
</div> </div>
@@ -175,9 +175,7 @@ const CalendarsView = () => {
</div> </div>
</div> </div>
<div className="w-full border-t border-gray-200"> <div className="w-full border-t border-gray-200">
<p className="px-2 pt-4 text-sm text-neutral-500"> <p className="px-2 pt-4 text-sm text-gray-500">{t("toggle_calendars_conflict")}</p>
{t("toggle_calendars_conflict")}
</p>
<ul className="space-y-2 p-4"> <ul className="space-y-2 p-4">
{item.calendars.map((cal) => ( {item.calendars.map((cal) => (
<CalendarSwitch <CalendarSwitch
@@ -13,6 +13,7 @@ import {
Dropdown, Dropdown,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownItem,
DropdownMenuTrigger, DropdownMenuTrigger,
Icon, Icon,
List, List,
@@ -83,7 +84,7 @@ const ConferencingLayout = () => {
} }
<div className="flex-grow truncate pl-2"> <div className="flex-grow truncate pl-2">
<ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse"> <ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse">
<h3 className="truncate text-sm font-medium text-neutral-900">{app.title}</h3> <h3 className="truncate text-sm font-medium text-gray-900">{app.title}</h3>
</ListItemTitle> </ListItemTitle>
<ListItemText component="p">{app.description}</ListItemText> <ListItemText component="p">{app.description}</ListItemText>
</div> </div>
@@ -94,16 +95,17 @@ const ConferencingLayout = () => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem> <DropdownMenuItem>
<Button <DropdownItem
type="button"
color="destructive" color="destructive"
StartIcon={Icon.FiTrash}
disabled={app.isGlobal} disabled={app.isGlobal}
StartIcon={Icon.FiTrash}
onClick={() => { onClick={() => {
setDeleteCredentialId(app.credentialIds[0]); setDeleteCredentialId(app.credentialIds[0]);
setDeleteAppModal(true); setDeleteAppModal(true);
}}> }}>
{t("remove_app")} {t("remove_app")}
</Button> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</Dropdown> </Dropdown>
@@ -181,7 +181,9 @@ const ProfileView = () => {
}; };
if (isLoading || !user || isLoadingAvatar || !avatar) if (isLoading || !user || isLoadingAvatar || !avatar)
return <SkeletonLoader title={t("profile")} description={t("profile_description")} />; return (
<SkeletonLoader title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
);
const defaultValues = { const defaultValues = {
username: user.username || "", username: user.username || "",
@@ -227,11 +229,7 @@ const ProfileView = () => {
{/* Delete account Dialog */} {/* Delete account Dialog */}
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}> <Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button data-testid="delete-account" color="destructive" className="mt-1" StartIcon={Icon.FiTrash2}>
data-testid="delete-account"
color="destructive"
className="mt-1 border-2"
StartIcon={Icon.FiTrash2}>
{t("delete_account")} {t("delete_account")}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
+11 -1
View File
@@ -92,7 +92,11 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
{...register("username")} {...register("username")}
required required
/> />
<EmailField {...register("email")} /> <EmailField
{...register("email")}
disabled={prepopulateFormValues?.email}
className="disabled:bg-gray-200 disabled:hover:cursor-not-allowed"
/>
<PasswordField <PasswordField
labelProps={{ labelProps={{
className: "block text-sm font-medium text-gray-700", className: "block text-sm font-medium text-gray-700",
@@ -145,6 +149,12 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
prepopulateFormValues: undefined, prepopulateFormValues: undefined,
}; };
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
return {
notFound: true,
};
}
// no token given, treat as a normal signup without verification token // no token given, treat as a normal signup without verification token
if (!token) { if (!token) {
return { return {
+47 -43
View File
@@ -12,13 +12,12 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { Avatar, Button, Icon, HeadSeo } from "@calcom/ui"; import { Avatar, Button, Icon, HeadSeo, AvatarGroup } from "@calcom/ui";
import { useToggleQuery } from "@lib/hooks/useToggleQuery"; import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import { inferSSRProps } from "@lib/types/inferSSRProps"; import { inferSSRProps } from "@lib/types/inferSSRProps";
import Team from "@components/team/screens/Team"; import Team from "@components/team/screens/Team";
import AvatarGroup from "@components/ui/AvatarGroup";
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>; export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({ team }: TeamPageProps) { function TeamPage({ team }: TeamPageProps) {
@@ -45,28 +44,28 @@ function TeamPage({ team }: TeamPageProps) {
"dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600", "dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600",
!isEmbed && "bg-white" !isEmbed && "bg-white"
)}> )}>
<Link href={`/team/${team.slug}/${type.slug}`}> <Link
<a className="flex justify-between px-6 py-4" data-testid="event-type-link"> href={`/team/${team.slug}/${type.slug}`}
<div className="flex-shrink"> className="flex justify-between px-6 py-4"
<div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse"> data-testid="event-type-link">
<h2 className="dark:text-darkgray-700 text-sm font-semibold text-gray-700">{type.title}</h2> <div className="flex-shrink">
</div> <div className="flex flex-wrap items-center space-x-2 rtl:space-x-reverse">
<EventTypeDescription className="text-sm" eventType={type} /> <h2 className="dark:text-darkgray-700 text-sm font-semibold text-gray-700">{type.title}</h2>
</div> </div>
<div className="mt-1 self-center"> <EventTypeDescription className="text-sm" eventType={type} />
<AvatarGroup </div>
border="border-2 border-white dark:border-darkgray-100" <div className="mt-1 self-center">
truncateAfter={4} <AvatarGroup
className="flex flex-shrink-0" truncateAfter={4}
size={10} className="flex flex-shrink-0"
items={type.users.map((user) => ({ size="sm"
alt: user.name || "", items={type.users.map((user) => ({
title: user.name || "", alt: user.name || "",
image: CAL_URL + "/" + user.username + "/avatar.png" || "", title: user.name || "",
}))} image: CAL_URL + "/" + user.username + "/avatar.png" || "",
/> }))}
</div> />
</a> </div>
</Link> </Link>
</li> </li>
))} ))}
@@ -97,27 +96,32 @@ function TeamPage({ team }: TeamPageProps) {
{!showMembers.isOn && team.eventTypes.length > 0 && ( {!showMembers.isOn && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl "> <div className="mx-auto max-w-3xl ">
<EventTypes /> <EventTypes />
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="dark:border-darkgray-300 w-full border-t border-gray-200" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-gray-100 px-2 text-sm text-gray-500 dark:text-white">
{t("or")}
</span>
</div>
</div>
<aside className="mt-8 flex justify-center text-center dark:text-white"> {!team.hideBookATeamMember && (
<Button <div>
color="minimal" <div className="relative mt-12">
EndIcon={Icon.FiArrowRight} <div className="absolute inset-0 flex items-center" aria-hidden="true">
className="dark:hover:bg-darkgray-200" <div className="dark:border-darkgray-300 w-full border-t border-gray-200" />
href={`/team/${team.slug}?members=1`} </div>
shallow={true}> <div className="relative flex justify-center">
{t("book_a_team_member")} <span className="dark:bg-darkgray-50 bg-gray-100 px-2 text-sm text-gray-500 dark:text-white">
</Button> {t("or")}
</aside> </span>
</div>
</div>
<aside className="mt-8 flex justify-center text-center dark:text-white">
<Button
color="minimal"
EndIcon={Icon.FiArrowRight}
className="dark:hover:bg-darkgray-200"
href={`/team/${team.slug}?members=1`}
shallow={true}>
{t("book_a_team_member")}
</Button>
</aside>
</div>
)}
</div> </div>
)} )}
</main> </main>
+19 -14
View File
@@ -51,16 +51,21 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
id: true, id: true,
slug: true, slug: true,
hidden: true, hidden: true,
users: { hosts: {
select: { select: {
id: true, isFixed: true,
name: true, user: {
avatar: true, select: {
username: true, id: true,
timeZone: true, name: true,
hideBranding: true, avatar: true,
brandColor: true, username: true,
darkBrandColor: true, timeZone: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
},
},
}, },
}, },
title: true, title: true,
@@ -124,11 +129,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
periodEndDate: eventType.periodEndDate?.toString() ?? null, periodEndDate: eventType.periodEndDate?.toString() ?? null,
recurringEvent: parseRecurringEvent(eventType.recurringEvent), recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations), locations: privacyFilteredLocations(locations),
users: eventType.users.map((user) => ({ users: eventType.hosts.map(({ user: { name, username, hideBranding, timeZone } }) => ({
name: user.name, name,
username: user.username, username,
hideBranding: user.hideBranding, hideBranding,
timeZone: user.timeZone, timeZone,
})), })),
}); });
+2 -1
View File
@@ -1,9 +1,10 @@
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import { TeamsListing } from "@calcom/features/ee/teams/components"; import { TeamsListing } from "@calcom/features/ee/teams/components";
import Shell from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, Shell } from "@calcom/ui"; import { Button, Icon } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
+11
View File
@@ -14,6 +14,17 @@ test.describe("App Store - Authed", () => {
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible(); await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
await pro.delete(); await pro.delete();
}); });
test("Installed Apps - Navigation", async ({ page, users }) => {
const user = await users.create();
await user.login();
await page.goto("/apps/installed");
await page.waitForSelector('[data-testid="connect-calendar-apps"]');
await page.click('[data-testid="vertical-tab-payment"]');
await page.waitForSelector('[data-testid="connect-payment-apps"]');
await page.click('[data-testid="vertical-tab-automation"]');
await page.waitForSelector('[data-testid="connect-automation-apps"]');
});
}); });
test.describe("App Store - Unauthed", () => { test.describe("App Store - Unauthed", () => {
+2 -1
View File
@@ -5,7 +5,8 @@ import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth } from "./lib/testU
test.describe.configure({ mode: "parallel" }); test.describe.configure({ mode: "parallel" });
test.describe("hash my url", () => { // TODO: This test is very flaky. Feels like tossing a coin and hope that it won't fail. Needs to be revisited.
test.fixme("hash my url", () => {
test.beforeEach(async ({ users }) => { test.beforeEach(async ({ users }) => {
const user = await users.create(); const user = await users.create();
await user.login(); await user.login();
@@ -1077,13 +1077,13 @@
"new_seat_subject": "حضور جديد {{name}} لـ {{eventType}} في {{date}}", "new_seat_subject": "حضور جديد {{name}} لـ {{eventType}} في {{date}}",
"new_seat_title": "شخص ما أضاف نفسه إلى حدث", "new_seat_title": "شخص ما أضاف نفسه إلى حدث",
"variable": "متغير", "variable": "متغير",
"event_name_workflow": "اسم الحدث", "event_name_variable": "اسم الحدث",
"organizer_name_workflow": "المنظِّم", "organizer_name_variable": "المنظِّم",
"attendee_name_workflow": "الحاضر", "attendee_name_variable": "الحاضر",
"event_date_workflow": "تاريخ الحدث", "event_date_variable": "تاريخ الحدث",
"event_time_workflow": "وقت الحدث", "event_time_variable": "وقت الحدث",
"location_workflow": "الموقع", "location_variable": "الموقع",
"additional_notes_workflow": "ملاحظات إضافية", "additional_notes_variable": "ملاحظات إضافية",
"app_upgrade_description": "لاستخدام هذه الميزة، تحتاج إلى الترقية إلى حساب Pro.", "app_upgrade_description": "لاستخدام هذه الميزة، تحتاج إلى الترقية إلى حساب Pro.",
"invalid_number": "رقم الهاتف غير صالح", "invalid_number": "رقم الهاتف غير صالح",
"navigate": "تنقّل", "navigate": "تنقّل",
@@ -1387,7 +1387,7 @@
"team_url_taken": "عنوان URL هذا مأخوذ بالفعل", "team_url_taken": "عنوان URL هذا مأخوذ بالفعل",
"team_publish": "نشر فريق", "team_publish": "نشر فريق",
"number_sms_notifications": "رقم الهاتف (إشعارات الرسائل النصية)", "number_sms_notifications": "رقم الهاتف (إشعارات الرسائل النصية)",
"attendee_email_workflow": "اسم الحاضر", "attendee_email_variable": "اسم الحاضر",
"attendee_email_info": "البريد الإلكتروني للشخص الحجز", "attendee_email_info": "البريد الإلكتروني للشخص الحجز",
"kbar_search_placeholder": "اكتب أمرًا أو بحثًا...", "kbar_search_placeholder": "اكتب أمرًا أو بحثًا...",
"invalid_credential": "لا! يبدو أن الصلاحية انتهت أو تم إلغاؤها. يرجى إعادة التثبيت مرة أخرى.", "invalid_credential": "لا! يبدو أن الصلاحية انتهت أو تم إلغاؤها. يرجى إعادة التثبيت مرة أخرى.",
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Událost stále čeká na schválení", "event_still_awaiting_approval": "Událost stále čeká na schválení",
"booking_submitted_subject": "Rezervace odeslána: {{eventType}} s {{name}} během {{date}}", "booking_submitted_subject": "Rezervace odeslána: {{eventType}} s {{name}} během {{date}}",
"your_meeting_has_been_booked": "Vaše schůzka byla rezervována", "your_meeting_has_been_booked": "Vaše schůzka byla rezervována",
"event_type_has_been_rescheduled_on_time_date": "Váš {{eventType}} s {{name}} byl přesunut na {{date}}.", "event_type_has_been_rescheduled_on_time_date": "Váš {{title}} byl přeplánován na {{date}}.",
"event_has_been_rescheduled": "Změna - Vaše událost byla přesunuta na jindy", "event_has_been_rescheduled": "Změna - Vaše událost byla přesunuta na jindy",
"request_reschedule_title_attendee": "Žádost o přeplánování rezervace", "request_reschedule_title_attendee": "Žádost o přeplánování rezervace",
"request_reschedule_subtitle": "Organizátor {{organizer}} zrušil rezervaci a požádal vás, abyste si vybrali jiný čas.", "request_reschedule_subtitle": "Organizátor {{organizer}} zrušil rezervaci a požádal vás, abyste si vybrali jiný čas.",
@@ -1076,13 +1076,13 @@
"new_seat_subject": "Nový účastník {{name}} události {{eventType}} dne {{date}}", "new_seat_subject": "Nový účastník {{name}} události {{eventType}} dne {{date}}",
"new_seat_title": "Někdo se přidal k události", "new_seat_title": "Někdo se přidal k události",
"variable": "Proměnná", "variable": "Proměnná",
"event_name_workflow": "Název události", "event_name_variable": "Název události",
"organizer_name_workflow": "Jméno organizátora", "organizer_name_variable": "Jméno organizátora",
"attendee_name_workflow": "Jméno účastníka", "attendee_name_variable": "Jméno účastníka",
"event_date_workflow": "Datum události", "event_date_variable": "Datum události",
"event_time_workflow": "Čas události", "event_time_variable": "Čas události",
"location_workflow": "Místo", "location_variable": "Místo",
"additional_notes_workflow": "Doplňující poznámky", "additional_notes_variable": "Doplňující poznámky",
"app_upgrade_description": "Pokud chcete použít tuto funkci, musíte provést aktualizaci na účet Pro.", "app_upgrade_description": "Pokud chcete použít tuto funkci, musíte provést aktualizaci na účet Pro.",
"invalid_number": "Neplatné telefonní číslo", "invalid_number": "Neplatné telefonní číslo",
"navigate": "Navigace", "navigate": "Navigace",
@@ -1386,7 +1386,7 @@
"team_url_taken": "Tato adresa URL je už obsazená", "team_url_taken": "Tato adresa URL je už obsazená",
"team_publish": "Zveřejnit tým", "team_publish": "Zveřejnit tým",
"number_sms_notifications": "Číslo telefonu (SMS oznámení)", "number_sms_notifications": "Číslo telefonu (SMS oznámení)",
"attendee_email_workflow": "E-mail účastníka", "attendee_email_variable": "E-mail účastníka",
"attendee_email_info": "E-mail osoby provádějící rezervaci", "attendee_email_info": "E-mail osoby provádějící rezervaci",
"kbar_search_placeholder": "Zadejte příkaz nebo vyhledejte...", "kbar_search_placeholder": "Zadejte příkaz nebo vyhledejte...",
"invalid_credential": "Ale ne! Zdá se, že platnost oprávnění vypršela nebo byla zrušena. Proveďte novou instalaci.", "invalid_credential": "Ale ne! Zdá se, že platnost oprávnění vypršela nebo byla zrušena. Proveďte novou instalaci.",
@@ -67,7 +67,7 @@
"event_still_awaiting_approval": "Ein Termin wartet noch auf Ihre Bestätigung", "event_still_awaiting_approval": "Ein Termin wartet noch auf Ihre Bestätigung",
"booking_submitted_subject": "Buchung eingereicht: {{eventType}} mit {{name}} am {{date}}", "booking_submitted_subject": "Buchung eingereicht: {{eventType}} mit {{name}} am {{date}}",
"your_meeting_has_been_booked": "Dein Termin wurde gebucht", "your_meeting_has_been_booked": "Dein Termin wurde gebucht",
"event_type_has_been_rescheduled_on_time_date": "Ihr {{eventType}} mit {{name}} wurde auf {{time}} ({{timeZone}}) am {{date}} verschoben.", "event_type_has_been_rescheduled_on_time_date": "Ihr {{title}} wurde auf den {{date}} verschoben.",
"event_has_been_rescheduled": "Ihr Termin wurde verschoben.", "event_has_been_rescheduled": "Ihr Termin wurde verschoben.",
"request_reschedule_title_attendee": "Anfrage zur Neuplanung Ihrer Buchung", "request_reschedule_title_attendee": "Anfrage zur Neuplanung Ihrer Buchung",
"request_reschedule_subtitle": "{{organizer}} hat die Buchung storniert und Sie gebeten, eine andere Zeit auszuwählen.", "request_reschedule_subtitle": "{{organizer}} hat die Buchung storniert und Sie gebeten, eine andere Zeit auszuwählen.",
@@ -1078,13 +1078,13 @@
"new_seat_subject": "Neuer Teilnehmer {{name}} beim {{eventType}} am {{date}}", "new_seat_subject": "Neuer Teilnehmer {{name}} beim {{eventType}} am {{date}}",
"new_seat_title": "Jemand hat sich zu einem Ereignis hinzugefügt", "new_seat_title": "Jemand hat sich zu einem Ereignis hinzugefügt",
"variable": "Variable", "variable": "Variable",
"event_name_workflow": "Event Name", "event_name_variable": "Event Name",
"organizer_name_workflow": "Organisator", "organizer_name_variable": "Organisator",
"attendee_name_workflow": "Teilnehmer", "attendee_name_variable": "Teilnehmer",
"event_date_workflow": "Event Datum", "event_date_variable": "Event Datum",
"event_time_workflow": "Event Zeit", "event_time_variable": "Event Zeit",
"location_workflow": "Ort", "location_variable": "Ort",
"additional_notes_workflow": "Zusätzliche Notizen", "additional_notes_variable": "Zusätzliche Notizen",
"app_upgrade_description": "Um diese Funktion nutzen zu können, müssen Sie ein Upgrade auf einen Pro-Account durchführen.", "app_upgrade_description": "Um diese Funktion nutzen zu können, müssen Sie ein Upgrade auf einen Pro-Account durchführen.",
"invalid_number": "Ungültige Telefonnummer", "invalid_number": "Ungültige Telefonnummer",
"navigate": "Navigieren", "navigate": "Navigieren",
@@ -1388,7 +1388,7 @@
"team_url_taken": "Diese URL ist bereits vergeben", "team_url_taken": "Diese URL ist bereits vergeben",
"team_publish": "Team veröffentlichen", "team_publish": "Team veröffentlichen",
"number_sms_notifications": "Telefonnummer (SMS-Benachrichtigungen)", "number_sms_notifications": "Telefonnummer (SMS-Benachrichtigungen)",
"attendee_email_workflow": "Teilnehmer E-Mail", "attendee_email_variable": "Teilnehmer E-Mail",
"attendee_email_info": "Die E-Mail-Adresse der buchenden Person", "attendee_email_info": "Die E-Mail-Adresse der buchenden Person",
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie...", "kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie...",
"invalid_credential": "Oh nein! Es sieht so aus, als ob die Berechtigung abgelaufen ist oder entzogen wurden. Bitte erneut installieren.", "invalid_credential": "Oh nein! Es sieht so aus, als ob die Berechtigung abgelaufen ist oder entzogen wurden. Bitte erneut installieren.",
+167 -1
View File
@@ -38,20 +38,37 @@
"confirm_or_reject_request": "Επιβεβαιώστε ή απορρίψτε το αίτημα", "confirm_or_reject_request": "Επιβεβαιώστε ή απορρίψτε το αίτημα",
"check_bookings_page_to_confirm_or_reject": "Ελέγξτε τη σελίδα κρατήσεων για να επιβεβαιώσετε ή να απορρίψετε την κράτηση.", "check_bookings_page_to_confirm_or_reject": "Ελέγξτε τη σελίδα κρατήσεων για να επιβεβαιώσετε ή να απορρίψετε την κράτηση.",
"event_awaiting_approval": "Ένα γεγονός περιμένει την έγκρισή σας", "event_awaiting_approval": "Ένα γεγονός περιμένει την έγκρισή σας",
"ics_event_title": "{{eventType}} με {{name}}",
"join_by_entrypoint": "Συμμετάσχετε από {{entryPoint}}",
"notes": "Σημειώσεις",
"manage_my_bookings": "Διαχείριση κρατήσεων",
"event_type": "Τύπος Συμβάντος", "event_type": "Τύπος Συμβάντος",
"meeting_password": "Συνθηματικό Συνάντησης", "meeting_password": "Συνθηματικό Συνάντησης",
"meeting_url": "URL Συνάντησης", "meeting_url": "URL Συνάντησης",
"meeting_request_rejected": "Το αίτημα συνάντησής σας απορρίφθηκε", "meeting_request_rejected": "Το αίτημα συνάντησής σας απορρίφθηκε",
"hi": "Γεια", "hi": "Γεια",
"join_team": "Γίνε μέλος της ομάδας",
"manage_this_team": "Διαχείριση αυτής της ομάδας", "manage_this_team": "Διαχείριση αυτής της ομάδας",
"team_info": "Πληροφορίες Ομάδας", "team_info": "Πληροφορίες Ομάδας",
"request_another_invitation_email": "Αν προτιμάτε να μην χρησιμοποιείτε το {{toEmail}} ως {{appName}} email σας ή να έχετε ήδη λογαριασμό {{appName}} , παρακαλώ ζητήστε μια άλλη πρόσκληση σε αυτό το email.",
"you_have_been_invited": "Έχετε προσκληθεί να γίνετε μέλος της ομάδας {{teamName}}",
"user_invited_you": "Ο χρήστης {{user}} σας προσκάλεσε να συμμετάσχετε στην ομάδα {{team}} στο {{appName}}",
"hidden_team_member_title": "Είστε κρυμμένοι σε αυτήν την ομάδα", "hidden_team_member_title": "Είστε κρυμμένοι σε αυτήν την ομάδα",
"link_expires": "Υ.Γ. Λήγει σε {{expiresIn}} ώρες.",
"team_upgrade_banner_action": "Αναβάθμισε εδώ",
"team_upgraded_successfully": "Η ομάδα σας αναβαθμίστηκε επιτυχώς!",
"use_link_to_reset_password": "Χρησιμοποιήστε τον παρακάτω σύνδεσμο για να επαναφέρετε τον κωδικό πρόσβασης",
"hey_there": "Γεια σας,",
"forgot_your_password_calcom": "Ξεχάσατε τον κωδικό πρόσβασης? - {{appName}}",
"confirm_delete_webhook": "Ναι, διαγραφή webhook",
"edit_webhook": "Επεξεργασία Webhook", "edit_webhook": "Επεξεργασία Webhook",
"delete_webhook": "Διαγραφή Webhook", "delete_webhook": "Διαγραφή Webhook",
"webhook_status": "Κατάσταση Webhook",
"webhook_enabled": "Ενεργοποιημένο Webhook", "webhook_enabled": "Ενεργοποιημένο Webhook",
"webhook_disabled": "Απενεργοποιημένο Webhook", "webhook_disabled": "Απενεργοποιημένο Webhook",
"webhook_response": "Απάντηση Webhook", "webhook_response": "Απάντηση Webhook",
"webhook_test": "Έλεγχος Webhook", "webhook_test": "Έλεγχος Webhook",
"manage_your_webhook": "Διαχείριση του webhook σας",
"webhook_created_successfully": "Το Webhook δημιουργήθηκε επιτυχώς!", "webhook_created_successfully": "Το Webhook δημιουργήθηκε επιτυχώς!",
"webhook_updated_successfully": "Το Webhook ενημερώθηκε επιτυχώς!", "webhook_updated_successfully": "Το Webhook ενημερώθηκε επιτυχώς!",
"webhook_removed_successfully": "Το Webhook αφαιρέθηκε επιτυχώς!", "webhook_removed_successfully": "Το Webhook αφαιρέθηκε επιτυχώς!",
@@ -62,6 +79,14 @@
"past": "Παρελθοντικά", "past": "Παρελθοντικά",
"choose_a_file": "Επιλογή αρχείου...", "choose_a_file": "Επιλογή αρχείου...",
"upload_image": "Μεταφόρτωση εικόνας", "upload_image": "Μεταφόρτωση εικόνας",
"view_notifications": "Εμφάνιση ειδοποιήσεων",
"view_public_page": "Προβολή δημόσιας σελίδας",
"copy_public_page_link": "Αντιγραφή συνδέσμου δημόσιας σελίδας",
"sign_out": "Αποσύνδεση",
"2fa_confirm_current_password": "Επιβεβαιώστε τον κωδικό πρόσβασης σας για να ξεκινήσετε.",
"required": "Υποχρεωτικό",
"optional": "Προαιρετικό",
"guests": "Επισκέπτες",
"username": "Όνομα χρήστη", "username": "Όνομα χρήστη",
"is_still_available": "είναι ακόμα διαθέσιμο.", "is_still_available": "είναι ακόμα διαθέσιμο.",
"documentation": "Τεκμηρίωση", "documentation": "Τεκμηρίωση",
@@ -88,17 +113,37 @@
"dont_have_an_account": "Δεν έχετε λογαριασμό;", "dont_have_an_account": "Δεν έχετε λογαριασμό;",
"sign_in_account": "Συνδεθείτε στο λογαριασμό σας", "sign_in_account": "Συνδεθείτε στο λογαριασμό σας",
"sign_in": "Είσοδος", "sign_in": "Είσοδος",
"error_during_login": "Παρουσιάστηκε σφάλμα κατά τη σύνδεσή σας. Επιστρέψτε στην οθόνη σύνδεσης και προσπαθήστε ξανά.",
"request_password_reset": "Αποστολή email επαναφοράς",
"forgot_password": "Ξεχάσατε τον κωδικό πρόσβασης?",
"all_apps": "Όλα",
"check_email_reset_password": "Ελέγξτε το email σας. Σας στείλαμε ένα σύνδεσμο για να επαναφέρετε τον κωδικό πρόσβασής σας.",
"few_sentences_about_yourself": "Μερικά λόγια για τον εσάς. Αυτό θα εμφανιστεί στην προσωπική σελίδα σας.",
"set_availability": "Όρισε τη διαθεσιμότητά σου",
"details": "Λεπτομέρειες",
"welcome": "Καλωσορίσατε",
"welcome_back": "Καλωσορίσατε πίσω",
"welcome_to_calcom": "Καλωσορίσατε στο {{appName}}",
"connect_caldav": "Σύνδεση στο CalDav (Beta)",
"credentials_stored_and_encrypted": "Τα διαπιστευτήριά σας θα αποθηκευτούν και θα κρυπτογραφηθούν.",
"connect": "Σύνδεση", "connect": "Σύνδεση",
"try_for_free": "Δοκιμάστε δωρεάν", "try_for_free": "Δοκιμάστε δωρεάν",
"add_to_calendar": "Προσθήκη στο ημερολόγιο", "add_to_calendar": "Προσθήκη στο ημερολόγιο",
"add_another_calendar": "Προσθήκη άλλου ημερολογίου", "add_another_calendar": "Προσθήκη άλλου ημερολογίου",
"email_sign_in_subject": "Ο σύνδεσμος εισόδου για {{appName}}",
"meeting_is_scheduled": "Η συνάντηση έχει προγραμματιστεί", "meeting_is_scheduled": "Η συνάντηση έχει προγραμματιστεί",
"submitted": "Η κράτησή σας έχει υποβληθεί", "submitted": "Η κράτησή σας έχει υποβληθεί",
"booking_submitted": "Η κράτησή σας έχει υποβληθεί",
"booking_confirmed": "Η κράτησή σας έχει επιβεβαιωθεί",
"booking_confirmed_recurring": "Η επαναλαμβανόμενη συνάντησή σας έχει επιβεβαιωθεί",
"warning_recurring_event_payment": "Οι πληρωμές δεν υποστηρίζονται ακόμη στις επαναλαμβανόμενες εκδηλώσεις",
"enter_new_password": "Εισάγετε τον νέο κωδικό πρόσβασης που θέλετε για το λογαριασμό σας.",
"reset_password": "Επαναφορά Κωδικού Πρόσβασης", "reset_password": "Επαναφορά Κωδικού Πρόσβασης",
"change_your_password": "Αλλαγή κωδικού πρόσβασης", "change_your_password": "Αλλαγή κωδικού πρόσβασης",
"show_password": "Εμφάνιση κωδικού πρόσβασης", "show_password": "Εμφάνιση κωδικού πρόσβασης",
"hide_password": "Απόκρυψη κωδικού πρόσβασης", "hide_password": "Απόκρυψη κωδικού πρόσβασης",
"try_again": "Δοκιμάστε ξανά", "try_again": "Δοκιμάστε ξανά",
"request_is_expired": "Το αίτημα έχει λήξει.",
"sunday_time_error": "Μη έγκυρη ώρα την Κυριακή", "sunday_time_error": "Μη έγκυρη ώρα την Κυριακή",
"monday_time_error": "Μη έγκυρη ώρα τη Δευτέρα", "monday_time_error": "Μη έγκυρη ώρα τη Δευτέρα",
"tuesday_time_error": "Μη έγκυρη ώρα την Τρίτη", "tuesday_time_error": "Μη έγκυρη ώρα την Τρίτη",
@@ -106,6 +151,127 @@
"thursday_time_error": "Μη έγκυρη ώρα την Πέμπτη", "thursday_time_error": "Μη έγκυρη ώρα την Πέμπτη",
"friday_time_error": "Μη έγκυρη ώρα την Παρασκευή", "friday_time_error": "Μη έγκυρη ώρα την Παρασκευή",
"saturday_time_error": "Μη έγκυρη ώρα το Σάββατο", "saturday_time_error": "Μη έγκυρη ώρα το Σάββατο",
"error_with_status_code_occured": "Παρουσιάστηκε ένα σφάλμα με κωδικό κατάστασης {{status}}.",
"booking_already_cancelled": "Αυτή η κράτηση είχε ήδη ακυρωθεί",
"booking_already_accepted_rejected": "Αυτή η κράτηση έγινε ήδη αποδεκτή ή απορρίφθηκε",
"go_back_home": "Επιστροφή στην αρχική σελίδα",
"or_go_back_home": "Ή επιστροφή στην αρχική σελίδα",
"no_meeting_found": "Δε βρέθηκε συνάντηση", "no_meeting_found": "Δε βρέθηκε συνάντηση",
"bookings": "Κρατήσεις" "no_status_bookings_yet": "Δεν υπάρχουν {{status}} κρατήσεις",
"no_status_bookings_yet_description": "Δεν έχετε {{status}} κρατήσεις. {{description}}",
"bookings": "Κρατήσεις",
"unconfirmed_bookings_tooltip": "Μη επιβεβαιωμένες κρατήσεις",
"start_time": "Ώρα έναρξης",
"end_time": "Ώρα λήξης",
"your_day_starts_at": "Η ημέρα σας ξεκινάει στις",
"your_day_ends_at": "Η ημέρα σας τελειώνει στις",
"change_available_times": "Αλλαγή διαθέσιμων ωρών",
"change_your_available_times": "Αλλάξτε τις διαθέσιμες ώρες σας",
"change_start_end": "Αλλάξτε την ώρα έναρξης και λήξης της ημέρας σας",
"current_start_date": "Αυτή τη στιγμή, η ημέρα σας έχει οριστεί να ξεκινήσει στις",
"email": "Email",
"email_placeholder": "jdoe@example.com",
"loading": "Φόρτωση...",
"deleting": "Διαγραφή...",
"developer": "Προγραμματιστές",
"schedule_a_meeting": "Προγραμματισμός συνάντησης",
"view_and_manage_billing_details": "Προβολή και διαχείριση των στοιχείων χρέωσης",
"view_and_edit_billing_details": "Προβολή και επεξεργασία των στοιχείων χρέωσης σας, καθώς και ακύρωση της συνδρομής σας.",
"need_anything_else": "Χρειάζεστε οτιδήποτε άλλο;",
"further_billing_help": "Αν χρειάζεστε περαιτέρω βοήθεια με την τιμολόγηση, η ομάδα υποστήριξής μας είναι εδώ για να βοηθήσει.",
"contact": "Επικοινωνία",
"contact_our_support_team": "Επικοινωνήστε με την ομάδα υποστήριξης",
"no_event_types_have_been_setup": "Αυτός ο χρήστης δεν έχει ορίσει ακόμα τύπους γεγονότων.",
"edit_logo": "Επεξεργασία λογοτύπου",
"upload_a_logo": "Ανεβάστε ένα λογότυπο",
"remove_logo": "Αφαίρεση λογότυπου",
"enable": "Ενεργοποίηση",
"2fa": "Ταυτοποίηση Δύο Παραμέτρων",
"security": "Ασφάλεια",
"manage_account_security": "Διαχειριστείτε την ασφάλεια του λογαριασμού σας.",
"password": "Κωδικός",
"password_updated_successfully": "Ο κωδικός πρόσβασης ενημερώθηκε επιτυχώς",
"password_has_been_changed": "Ο κωδικός πρόσβασής σας έχει αλλάξει με επιτυχία.",
"error_changing_password": "Σφάλμα στην αλλαγή κωδικού πρόσβασης",
"something_went_wrong": "Κάτι πήγε στραβά.",
"something_doesnt_look_right": "Κάτι δεν φαίνεται σωστά;",
"please_try_again": "Παρακαλώ δοκιμάστε ξανά.",
"super_secure_new_password": "Ο σούπερ ασφαλής νέος κωδικός πρόσβασης σας",
"new_password": "Νέος Κωδικός",
"your_old_password": "Ο παλιός σας κωδικός",
"current_password": "Τρέχων Κωδικός",
"change_password": "Αλλαγή Κωδικού",
"change_secret": "Αλλαγή Μυστικού",
"new_password_matches_old_password": "Ο νέος κωδικός πρόσβασης ταιριάζει με τον παλιό σας κωδικό. Παρακαλώ επιλέξτε διαφορετικό κωδικό πρόσβασης.",
"your_name": "Το όνομά σας",
"email_address": "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
"enter_valid_email": "Παρακαλώ εισάγετε ένα έγκυρο email",
"location": "Τοποθεσία",
"address": "Διεύθυνση",
"enter_address": "Εισάγετε διεύθυνση",
"yes": "ναι",
"no": "όχι",
"additional_notes": "Πρόσθετες σημειώσεις",
"your_number": "Ο αριθμός τηλεφώνου σας",
"phone_number": "Αριθμός Τηλεφώνου",
"attendee_phone_number": "Αριθμός Τηλεφώνου Συμμετέχοντα",
"host_phone_number": "Ο αριθμός τηλεφώνου σας",
"enter_phone_number": "Εισάγετε αριθμό τηλεφώνου",
"or": "Ή",
"go_back": "Επιστροφή",
"email_or_username": "Email ή Όνομα χρήστη",
"role": "Ρόλος",
"edit_role": "Επεξεργασία Ρόλου",
"edit_team": "Επεξεργασία ομάδας",
"reject": "Απορρίψτε",
"reject_all": "Απόρριψη όλων",
"accept": "Αποδοχή",
"leave": "Αποχώρηση",
"team_name": "Όνομα ομάδας",
"your_team_name": "Το όνομα της ομάδας σας",
"team_updated_successfully": "Επιτυχής ενημέρωση ομάδας",
"your_team_updated_successfully": "Η ομάδα σας ενημερώθηκε με επιτυχία.",
"about": "Σχετικά",
"confirm_remove_member": "Ναι, αφαίρεση μέλους",
"remove_member": "Αφαίρεση μέλους",
"manage_your_team": "Διαχείριση ομάδας",
"no_teams": "Δεν έχετε καμία ομάδα ακόμη.",
"submit": "Υποβολή",
"delete": "Διαγραφή",
"update": "Ενημέρωση",
"save": "Αποθήκευση",
"pending": "Εκκρεμεί",
"share": "Κοινοποίηση",
"leave_team": "Αποχώρηση από ομάδα",
"confirm_leave_team": "Ναι, αποχώρηση από την ομάδα",
"preview": "Προεπισκόπηση",
"link_copied": "Ο σύνδεσμος αντιγράφηκε!",
"private_link_copied": "Ο ιδιωτικός σύνδεσμος αντιγράφηκε!",
"title": "Τίτλος",
"description": "Περιγραφή",
"preview_team": "Προεπισκόπηση ομάδας",
"duration": "Διάρκεια",
"minutes": "Λεπτά",
"one_time_link": "Σύνδεσμος μιας χρήσης",
"first_day_of_week": "Πρώτη ημέρα της εβδομάδας",
"weekly_one": "εβδομάδα",
"weekly_other": "εβδομάδες",
"monthly_one": "μήνας",
"monthly_other": "μήνες",
"yearly_one": "έτος",
"yearly_other": "έτη",
"max": "Μέγιστο",
"create_team": "Δημιουργία ομάδας",
"name": "Όνομα",
"create_new_team": "Δημιουργία νέας ομάδας",
"new_team": "Νέα Ομάδα",
"teams": "Ομάδες",
"team": "Ομάδα",
"enabled": "Ενεργοποιημένο",
"disabled": "Απενεργοποιημένο",
"show_advanced_settings": "Εμφάνιση προχωρημένων ρυθμίσεων",
"event_name_tooltip": "Το όνομα που θα εμφανίζεται στα ημερολόγια",
"label": "Ετικέτα",
"edit": "Επεξεργασία",
"disable_guests": "Απενεργοποίηση επισκεπτών"
} }
+38 -16
View File
@@ -9,8 +9,8 @@
"calcom_explained": "{{appName}} provides scheduling infrastructure for absolutely everyone.", "calcom_explained": "{{appName}} provides scheduling infrastructure for absolutely everyone.",
"have_any_questions": "Have questions? We're here to help.", "have_any_questions": "Have questions? We're here to help.",
"reset_password_subject": "{{appName}}: Reset password instructions", "reset_password_subject": "{{appName}}: Reset password instructions",
"event_declined_subject": "Declined: {{eventType}} with {{name}} at {{date}}", "event_declined_subject": "Declined: {{title}} at {{date}}",
"event_cancelled_subject": "Cancelled: {{eventType}} with {{name}} at {{date}}", "event_cancelled_subject": "Cancelled: {{title}} at {{date}}",
"event_request_declined": "Your event request has been declined", "event_request_declined": "Your event request has been declined",
"event_request_declined_recurring": "Your recurring event request has been declined", "event_request_declined_recurring": "Your recurring event request has been declined",
"event_request_cancelled": "Your scheduled event was cancelled", "event_request_cancelled": "Your scheduled event was cancelled",
@@ -32,7 +32,7 @@
"refund_failed": "The refund for the event {{eventType}} with {{userName}} on {{date}} failed.", "refund_failed": "The refund for the event {{eventType}} with {{userName}} on {{date}} failed.",
"check_with_provider_and_user": "Please check with your payment provider and {{user}} how to handle this.", "check_with_provider_and_user": "Please check with your payment provider and {{user}} how to handle this.",
"a_refund_failed": "A refund failed", "a_refund_failed": "A refund failed",
"awaiting_payment_subject": "Awaiting Payment: {{eventType}} with {{name}} on {{date}}", "awaiting_payment_subject": "Awaiting Payment: {{title}} on {{date}}",
"meeting_awaiting_payment": "Your meeting is awaiting payment", "meeting_awaiting_payment": "Your meeting is awaiting payment",
"help": "Help", "help": "Help",
"price": "Price", "price": "Price",
@@ -64,11 +64,11 @@
"someone_requested_an_event": "Someone has requested to schedule an event on your calendar.", "someone_requested_an_event": "Someone has requested to schedule an event on your calendar.",
"someone_requested_password_reset": "Someone has requested a link to change your password.", "someone_requested_password_reset": "Someone has requested a link to change your password.",
"password_reset_instructions": "If you didn't request this, you can safely ignore this email and your password will not be changed.", "password_reset_instructions": "If you didn't request this, you can safely ignore this email and your password will not be changed.",
"event_awaiting_approval_subject": "Awaiting Approval: {{eventType}} with {{name}} at {{date}}", "event_awaiting_approval_subject": "Awaiting Approval: {{eventType}} at {{date}}",
"event_still_awaiting_approval": "An event is still waiting for your approval", "event_still_awaiting_approval": "An event is still waiting for your approval",
"booking_submitted_subject": "Booking Submitted: {{eventType}} with {{name}} at {{date}}", "booking_submitted_subject": "Booking Submitted: {{title}} at {{date}}",
"your_meeting_has_been_booked": "Your meeting has been booked", "your_meeting_has_been_booked": "Your meeting has been booked",
"event_type_has_been_rescheduled_on_time_date": "Your {{eventType}} with {{name}} has been rescheduled to {{date}}.", "event_type_has_been_rescheduled_on_time_date": "Your {{title}} has been rescheduled to {{date}}.",
"event_has_been_rescheduled": "Updated - Your event has been rescheduled", "event_has_been_rescheduled": "Updated - Your event has been rescheduled",
"request_reschedule_title_attendee": "Request to reschedule your booking", "request_reschedule_title_attendee": "Request to reschedule your booking",
"request_reschedule_subtitle": "{{organizer}} has cancelled the booking and requested you to pick another time.", "request_reschedule_subtitle": "{{organizer}} has cancelled the booking and requested you to pick another time.",
@@ -521,6 +521,8 @@
"change_member_role": "Change team member role", "change_member_role": "Change team member role",
"disable_cal_branding": "Disable {{appName}} branding", "disable_cal_branding": "Disable {{appName}} branding",
"disable_cal_branding_description": "Hide all {{appName}} branding from your public pages.", "disable_cal_branding_description": "Hide all {{appName}} branding from your public pages.",
"hide_book_a_team_member": "Hide Book a Team Member Button",
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
"danger_zone": "Danger zone", "danger_zone": "Danger zone",
"back": "Back", "back": "Back",
"cancel": "Cancel", "cancel": "Cancel",
@@ -797,12 +799,14 @@
"no_category_apps_description_analytics": "Add an analytics app for your booking pages", "no_category_apps_description_analytics": "Add an analytics app for your booking pages",
"no_category_apps_description_automation": "Add an automation app to use", "no_category_apps_description_automation": "Add an automation app to use",
"no_category_apps_description_other": "Add any other type of app to do all sorts of things", "no_category_apps_description_other": "Add any other type of app to do all sorts of things",
"no_category_apps_description_web3": "Add a web3 app for your booking pages",
"installed_app_calendar_description": "Set the calendars to check for conflicts to prevent double bookings.", "installed_app_calendar_description": "Set the calendars to check for conflicts to prevent double bookings.",
"installed_app_conferencing_description": "Add your favourite video conferencing apps for your meetings", "installed_app_conferencing_description": "Add your favourite video conferencing apps for your meetings",
"installed_app_payment_description": "Configure which payment processing services to use when charging your clients.", "installed_app_payment_description": "Configure which payment processing services to use when charging your clients.",
"installed_app_analytics_description": "Configure which analytics apps to use for your booking pages", "installed_app_analytics_description": "Configure which analytics apps to use for your booking pages",
"installed_app_other_description": "All your installed apps from other categories.", "installed_app_other_description": "All your installed apps from other categories.",
"installed_app_automation_description": "Configure which automation apps to use", "installed_app_automation_description": "Configure which automation apps to use",
"installed_app_web3_description": "Configure which web3 apps to use for your booking pages",
"analytics": "Analytics", "analytics": "Analytics",
"empty_installed_apps_headline": "No apps installed", "empty_installed_apps_headline": "No apps installed",
"empty_installed_apps_description": "Apps enable you to enhance your workflow and improve your scheduling life significantly.", "empty_installed_apps_description": "Apps enable you to enhance your workflow and improve your scheduling life significantly.",
@@ -1082,13 +1086,13 @@
"new_seat_subject": "New Attendee {{name}} on {{eventType}} at {{date}}", "new_seat_subject": "New Attendee {{name}} on {{eventType}} at {{date}}",
"new_seat_title": "Someone has added themselves to an event", "new_seat_title": "Someone has added themselves to an event",
"variable": "Variable", "variable": "Variable",
"event_name_workflow": "Event name", "event_name_variable": "Event name",
"organizer_name_workflow": "Organizer", "organizer_name_variable": "Organizer",
"attendee_name_workflow": "Attendee", "attendee_name_variable": "Attendee",
"event_date_workflow": "Event date", "event_date_variable": "Event date",
"event_time_workflow": "Event time", "event_time_variable": "Event time",
"location_workflow": "Location", "location_variable": "Location",
"additional_notes_workflow": "Additional notes", "additional_notes_variable": "Additional notes",
"app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.", "app_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
"invalid_number": "Invalid phone number", "invalid_number": "Invalid phone number",
"navigate": "Navigate", "navigate": "Navigate",
@@ -1215,6 +1219,7 @@
"connect_automation_apps": "Connect automation apps", "connect_automation_apps": "Connect automation apps",
"connect_analytics_apps": "Connect analytics apps", "connect_analytics_apps": "Connect analytics apps",
"connect_other_apps": "Connect other apps", "connect_other_apps": "Connect other apps",
"connect_web3_apps": "Connect web3 apps",
"current_step_of_total": "Step {{currentStep}} of {{maxSteps}}", "current_step_of_total": "Step {{currentStep}} of {{maxSteps}}",
"add_variable": "Add variable", "add_variable": "Add variable",
"custom_phone_number": "Custom phone number", "custom_phone_number": "Custom phone number",
@@ -1402,7 +1407,7 @@
"team_url_taken": "This URL is already taken", "team_url_taken": "This URL is already taken",
"team_publish": "Publish team", "team_publish": "Publish team",
"number_sms_notifications": "Phone number (SMS\u00a0notifications)", "number_sms_notifications": "Phone number (SMS\u00a0notifications)",
"attendee_email_workflow": "Attendee email", "attendee_email_variable": "Attendee email",
"attendee_email_info": "The person booking's email", "attendee_email_info": "The person booking's email",
"kbar_search_placeholder": "Type a command or search...", "kbar_search_placeholder": "Type a command or search...",
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.", "invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.",
@@ -1454,10 +1459,11 @@
"enter_option": "Enter Option {{index}}", "enter_option": "Enter Option {{index}}",
"add_an_option": "Add an option", "add_an_option": "Add an option",
"radio": "Radio", "radio": "Radio",
"google_meet_warning": "In order to use Google Meet you must set your destination calendar to a Google Calendar",
"individual":"Individual", "individual":"Individual",
"all_bookings_filter_label":"All Bookings", "all_bookings_filter_label":"All Bookings",
"all_users_filter_label":"All Users", "all_users_filter_label":"All Users",
"meeting_url_workflow": "Meeting url", "meeting_url_variable": "Meeting url",
"meeting_url_info": "The event meeting conference url", "meeting_url_info": "The event meeting conference url",
"date_overrides": "Date overrides", "date_overrides": "Date overrides",
"date_overrides_subtitle": "Add dates when your availability changes from your daily hours.", "date_overrides_subtitle": "Add dates when your availability changes from your daily hours.",
@@ -1488,5 +1494,21 @@
"disable_success_page":"Disable Success Page (only works if you have a redirect URL)", "disable_success_page":"Disable Success Page (only works if you have a redirect URL)",
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters", "invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters",
"change_password_admin": "Change Password to gain admin access", "change_password_admin": "Change Password to gain admin access",
"username_already_taken": "Username is already taken" "username_already_taken": "Username is already taken",
"assignment": "Assignment",
"fixed_hosts": "Fixed Hosts",
"add_fixed_hosts": "Add fixed hosts",
"round_robin_hosts": "Round-Robin Hosts",
"minimum_round_robin_hosts_count": "Number of hosts required to attend",
"hosts": "Hosts",
"upgrade_to_enable_feature": "You need to create a team to enable this feature. Click to create a team.",
"new_attendee": "New Attendee",
"awaiting_approval": "Awaiting Approval",
"requires_google_calendar": "This app requires a Google Calendar connection",
"connected_google_calendar": "You have connected a Google Calendar account.",
"using_meet_requires_calendar": "Using Google Meet requires a connected Google Calendar",
"continue_to_install_google_calendar": "Continue to install Google Calendar",
"install_google_meet": "Install Google Meet",
"install_google_calendar": "Install Google Calendar",
"no_recordings_found": "No recordings found"
} }
+11 -11
View File
@@ -68,7 +68,7 @@
"event_still_awaiting_approval": "Un evento aún está esperando su aprobación", "event_still_awaiting_approval": "Un evento aún está esperando su aprobación",
"booking_submitted_subject": "Reserva enviada: {{eventType}} con {{name}} el {{date}}", "booking_submitted_subject": "Reserva enviada: {{eventType}} con {{name}} el {{date}}",
"your_meeting_has_been_booked": "Su reunión ha sido reservada", "your_meeting_has_been_booked": "Su reunión ha sido reservada",
"event_type_has_been_rescheduled_on_time_date": "Tu {{eventType}} con {{name}} se reprogramó para el {{date}}.", "event_type_has_been_rescheduled_on_time_date": "Tu {{title}} ha sido reprogramado para {{date}}.",
"event_has_been_rescheduled": "Tu evento ha sido reprogramado.", "event_has_been_rescheduled": "Tu evento ha sido reprogramado.",
"request_reschedule_title_attendee": "Solicitud para reprogramar su reserva", "request_reschedule_title_attendee": "Solicitud para reprogramar su reserva",
"request_reschedule_subtitle": "{{organizer}} ha cancelado la reserva y le ha solicitado que elija otra hora.", "request_reschedule_subtitle": "{{organizer}} ha cancelado la reserva y le ha solicitado que elija otra hora.",
@@ -511,7 +511,7 @@
"add_team_members_description": "Invitar a otros a unirse a tu equipo", "add_team_members_description": "Invitar a otros a unirse a tu equipo",
"add_team_member": "Añadir miembro del equipo", "add_team_member": "Añadir miembro del equipo",
"invite_new_member": "Invitar a un nuevo miembro", "invite_new_member": "Invitar a un nuevo miembro",
"invite_new_member_description": "Nota: Esto costará <1>un cupo adicional ($15/m)</1> en su suscripción.", "invite_new_member_description": "Nota: Esto costará <1>una plaza adicional ($15/m)</1> en su suscripción.",
"invite_new_team_member": "Invita a alguien a tu equipo.", "invite_new_team_member": "Invita a alguien a tu equipo.",
"change_member_role": "Cambiar rol del miembro del equipo", "change_member_role": "Cambiar rol del miembro del equipo",
"disable_cal_branding": "Desactivar marca de {{appName}}", "disable_cal_branding": "Desactivar marca de {{appName}}",
@@ -1076,13 +1076,13 @@
"new_seat_subject": "Nuevo asistente {{name}} en {{eventType}} el {{date}}", "new_seat_subject": "Nuevo asistente {{name}} en {{eventType}} el {{date}}",
"new_seat_title": "Alguien se ha añadido a un evento", "new_seat_title": "Alguien se ha añadido a un evento",
"variable": "Variable", "variable": "Variable",
"event_name_workflow": "Nombre del evento", "event_name_variable": "Nombre del evento",
"organizer_name_workflow": "Nombre del organizador", "organizer_name_variable": "Nombre del organizador",
"attendee_name_workflow": "Nombre del asistente", "attendee_name_variable": "Nombre del asistente",
"event_date_workflow": "Fecha del evento", "event_date_variable": "Fecha del evento",
"event_time_workflow": "Hora del evento", "event_time_variable": "Hora del evento",
"location_workflow": "Lugar", "location_variable": "Lugar",
"additional_notes_workflow": "Notas Adicionales", "additional_notes_variable": "Notas Adicionales",
"app_upgrade_description": "Para poder usar esta función, necesita actualizarse a una cuenta Pro.", "app_upgrade_description": "Para poder usar esta función, necesita actualizarse a una cuenta Pro.",
"invalid_number": "Número de teléfono no válido", "invalid_number": "Número de teléfono no válido",
"navigate": "Navegar", "navigate": "Navegar",
@@ -1386,7 +1386,7 @@
"team_url_taken": "Esta URL ya está en uso", "team_url_taken": "Esta URL ya está en uso",
"team_publish": "Publicar equipo", "team_publish": "Publicar equipo",
"number_sms_notifications": "Número de teléfono (notificaciones por SMS)", "number_sms_notifications": "Número de teléfono (notificaciones por SMS)",
"attendee_email_workflow": "Correo electrónico del asistente", "attendee_email_variable": "Correo electrónico del asistente",
"attendee_email_info": "El correo electrónico de la persona que reserva", "attendee_email_info": "El correo electrónico de la persona que reserva",
"kbar_search_placeholder": "Escriba un comando o búsqueda...", "kbar_search_placeholder": "Escriba un comando o búsqueda...",
"invalid_credential": "¡Oh no! Parece que el permiso ha caducado o se ha revocado. Vuelva a instalarlo.", "invalid_credential": "¡Oh no! Parece que el permiso ha caducado o se ha revocado. Vuelva a instalarlo.",
@@ -1396,7 +1396,7 @@
"sender_id": "ID del remitente", "sender_id": "ID del remitente",
"sender_id_error_message": "Sólo se permiten letras, números y espacios (máx. 11 caracteres)", "sender_id_error_message": "Sólo se permiten letras, números y espacios (máx. 11 caracteres)",
"test_routing_form": "Prueba de formulario de ruta", "test_routing_form": "Prueba de formulario de ruta",
"test_preview": "Vista previa de la prueba", "test_preview": "Probar vista previa",
"route_to": "Ruta hacia", "route_to": "Ruta hacia",
"test_preview_description": "Pruebe su formulario de ruta sin enviar ningún dato", "test_preview_description": "Pruebe su formulario de ruta sin enviar ningún dato",
"test_routing": "Prueba de ruta", "test_routing": "Prueba de ruta",
@@ -0,0 +1,79 @@
{
"day_one": "egun {{count}}",
"day_other": "{{count}} egun",
"second_one": "segundo {{count}}",
"second_other": "{{count}} segundo",
"accept_invitation": "Onartu gonbidapena",
"have_any_questions": "Galderarik? Laguntzeko gaude.",
"reset_password_subject": "{{appName}}: Pasahitza berrezartzeko argibideak",
"event_declined_subject": "Baztertua: {{title}} {{date}}(e)an",
"event_cancelled_subject": "Bertan behera: {{title}} {{date}}(e)an",
"event_request_declined": "Zure gertaera-eskaera baztertua izan da",
"event_request_declined_recurring": "Zure gertaera errepikari-eskaera baztertua izan da",
"event_request_cancelled": "Zure programatutako gertaera baztertua izan da",
"organizer": "Antolatzailea",
"need_to_reschedule_or_cancel": "Programazioa aldatu edo bertan behera utzi behar duzu?",
"cancellation_reason": "Bertan behera uztearen arrazoia (aukerakoa)",
"cancellation_reason_placeholder": "Zergatik utzi duzu bertan behera?",
"rejection_reason": "Errefusatzeko arrazoia",
"rejection_reason_title": "Errefusatu erreserba-eskaera?",
"rejection_reason_description": "Ziur zaude erreserba errefusatu nahi duzula? Erreserba-eskaera egin duen pertsonari jakinaraziko zaio. Arrazoi bat adieraz dezakezu behean.",
"rejection_confirmation": "Errefusatu erreserba",
"manage_this_event": "Kudeatu gertaera hau",
"your_event_has_been_scheduled": "Zure gertaera programatu da",
"error_message": "Errore-mezua honakoa ian da: '{{errorMessage}}'",
"refund_failed_subject": "Itzulketak huts egin du: {{name}} - {{date}} - {{eventType}}",
"refund_failed": "Huts egin du itzulketak {{eventType}} gertaerarako, {{userName}}(r)ekin {{date}}(e)an.",
"a_refund_failed": "Itzulketa batek huts egin du",
"awaiting_payment_subject": "Ordainketaren zain: {{title}} {{date}}(e)an",
"help": "Laguntza",
"price": "Prezioa",
"paid": "Ordainduta",
"refunded": "Itzulita",
"payment": "Ordainketa",
"pay_now": "Ordaindu orain",
"no_more_results": "Emaitza gehiagorik ez",
"no_results": "Emaitzarik ez",
"load_more_results": "Kargatu emaitza gehiago",
"confirmed_event_type_subject": "Baieztatua: {{eventType}} {{name}}(r)ekin {{date}}(e)an",
"confirm_or_reject_request": "Baieztatu edo errefusatu eskaera",
"check_bookings_page_to_confirm_or_reject": "Begiratu zure erreserba-orrialdea erreserba baieztatu edo errefusatzeko.",
"event_awaiting_approval": "Gertaera bat zure onarpenaren zain dago",
"booking_submitted_subject": "Erreserba bidalita: {{title}} {{date}}(e)an",
"event_type_has_been_rescheduled_on_time_date": "Zure {{title}} getaeraren programazioa aldatu egin da {{date}}(e)ra.",
"hi_user_name": "Kaixo {{name}}",
"ics_event_title": "{{eventType}} {{name}}(r)ekin",
"notes": "Oharrak",
"manage_my_bookings": "Kudeatu nire erreserbak",
"rejected_event_type_with_organizer": "Errefusatua: {{eventType}} {{organizer}}(r)ekin {{date}}(e)an",
"hi": "Kaixo",
"change_bookings_availability": "Aldatu noiz zauden prest erreserbak jasotzeko",
"create_booking_link_with_calcom": "Sor ezazu zeure erreserba-esteka {{appName}}(e)kin",
"user_needs_to_confirm_or_reject_booking": "{{user}}(e)k erreserba baieztatu edo errefusatu behar du oraindik.",
"booking_submitted": "Zure erreserba bidali da",
"booking_confirmed": "Zure erreserba baieztatu da",
"back_to_bookings": "Itzuli erreserbatara",
"really_cancel_booking": "Benetan bertan behera utzi nahi duzu zure erreserba?",
"cannot_cancel_booking": "Ezin duzu erreserba hau bertan behera utzi",
"booking_already_cancelled": "Erreserba hau bertan behera utzi da dagoeneko",
"booking_already_accepted_rejected": "Erreserba hau onartu edo errefusatu da dagoeneko",
"bookings": "Erreserbak",
"past_bookings": "Zure iraganeko erreserbak agertuko dira hemen.",
"cancelled_bookings": "Zure bertan behera utzitako erreserbak agertuko dira hemen.",
"unconfirmed_bookings": "Zure baieztatu gabeko erreserbak agertuko dira hemen.",
"unconfirmed_bookings_tooltip": "Baieztatu gabeko erreserbak",
"booking_cancelled": "Erreserba bertan behera",
"booking_rescheduled": "Erreserbaren programazioa aldatuta",
"booking_created": "Erreserba sortuta",
"edit_booking": "Aldatu erreserba",
"reschedule_booking": "Aldatu erreserbaren programazioa",
"location_changed_event_type_subject": "Kokapena aldatu da: {{eventType}} {{name}}(r)ekin {{date}}(e)an",
"booking_details": "Erreserbaren xehetasunak",
"reminder_email": "Gogorarazpena: {{eventType}} {{name}}(r)ekin {{date}}(e)an",
"new_seat_subject": "{{name}} parte-hartzaile berria {{eventType}}(e)n {{date}}(e)an",
"booking_appearance": "Erreserba-orriaren itxura",
"limit_booking_frequency": "Mugatu erreserbatzeko maiztasuna",
"booking_tentative": "Erreserba hau behin-behinekoa da",
"limit_future_bookings": "Mugatu etorkizuneko erreserbak",
"all_bookings_filter_label": "Erreserba guztiak"
}
File diff suppressed because it is too large. Load diff
Loaded 100 of 413 files, more files were not shown because too many files have changed in this diff. Show more