Compare commits

...
114 Commits
Author SHA1 Message Date
Alex van Andel 17a0a61313 v2.5.3
release.yml / release (push) Canceled after 0s
2023-01-22 00:06:41 +00:00
Alex van Andel 12b7739c9a Bugfix/dropdown menu trigger as child remove class names (#6614)
* Fix UsernameTextfield to take right height

* Remove className side-effect

* Incorrect resolution version fixed

* Converted mobile DropdownMenuTrigger styles into Button
2023-01-21 17:03:59 -07:00
Alex van Andel ff283a25d2 Reduce the avatar size to 'sm' for now 2023-01-21 22:09:32 +00:00
sean-brydon 4b343f20a4 Update booking filters design (#6543)
* Update booking filters

* Add filter on YOUR bookings

* Fix pending members showing up in list
2023-01-21 18:15:59 +01:00
Udit Takkar 5bb6904ffd feat: add LocationSelect component (#6571)
* feat: add LocationSelect component

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

* fix: type error

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

* chore: type error

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

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-21 16:52:21 +00:00
sean-brydon 0e04f5d338 Fix FAB (#6611) 2023-01-21 16:14:08 +00:00
Nafees Nazik da0ea5d9d0 fix: case-sensitivity in apps path (#6552)
* fix: lowercase slug

* fix: make fallback blocking
2023-01-21 16:13:55 +00:00
Peer Richelsen 1a19261b8f remove 1 on 1 icon (#6609)
* removed 1-on-1 badge

* changed user to users for group events
2023-01-21 16:07:45 +00:00
Luis Cadillo 55a6c65db6 fix: remove annoying outline when <Button /> clicked (#6537)
* fix: remove annoying outline when <Button /> clicked

* Delete yarn.lock
2023-01-21 16:07:30 +00:00
Alex van Andel c9ed3388ae Regenerated lockfile without upgrade (#6610) 2023-01-21 15:49:35 +00:00
2588b7ef84 Harsh/add member invite (#6598)
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
2023-01-21 16:32:00 +01:00
7ce83542e2 [CAL-811] Avatar icon not redirecting user back to the main page (#6586)
* Remove cursor-pointer, remove old Avatar* files

* Fixed styling for checkedSelect + some cleanup

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-21 11:41:49 +00:00
Alex van Andel 0bb0883e64 Fix string equality check in git-setup.sh 2023-01-21 06:24:29 +00:00
zomars d3be62fbdd Yarn lock fixes 2023-01-20 16:28:22 -07:00
zomars 736308e43a Test fixes 2023-01-20 15:10:09 -07:00
Peer Richelsen 791927e45e Update package.json
release.yml / release (push) Canceled after 0s
2023-01-20 23:06:43 +01:00
Alex van Andel fa984e1568 Changed a neutral- classes to gray (#6603)
* Changed a neutral- classes to gray

* Changed all border-1 to border
2023-01-20 23:04:58 +01:00
Peer Richelsen 9b197da5be Update README.md
release.yml / release (push) Canceled after 0s
2023-01-20 21:58:11 +01:00
Peer Richelsen b7c316b01b Update README.md 2023-01-20 21:57:52 +01:00
Udit Takkar 5ab56310bf fix: add delete in dropdown (#6599)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-20 14:57:14 -05:00
Udit Takkar 52e318ca18 fix: show button on empty string (#6601)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-20 12:27:58 -07:00
Omar López 8d078564dd Bye bye submodules (#6585)
* WIP

* Uses ssh instead

* Update .gitignore

* Update .gitignore

* Update Makefile

* Update git-setup.sh

* Update git-setup.sh

* Replaced Makefile with bash script

* Update package.json
2023-01-20 12:17:51 -07:00
7b5482e83d [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561)
* [CAL-808] /availability/single - UI issue on buttons beside time inputs

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

* Update packages/features/schedules/components/Schedule.tsx

* create new translation for tooltip

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-20 17:56:28 +00:00
sean-brydon 23086476b3 Tailwind Merge (#6596)
* Tailwind Merge

* Fix merge classNames
2023-01-20 15:19:12 +01:00
Nafees Nazik 00cab5c41f fix: react-select overflow issue in some modals. (#6587)
* feat: add a disable overflow prop

* feat: use the disable overflow prop
2023-01-20 13:29:37 +00:00
sean-brydon fbc27e1d52 Fix booking filters not working for admin (#6576) 2023-01-19 19:23:58 +01:00
Nafees Nazik e1c9a8cbfc fix: i18n placeholder in awaiting approval email (#6573) 2023-01-19 16:56:44 +01:00
Nafees Nazik 8ee109e975 fix: app list item ui issues in /settings/my-account/conferencing (#6565)
* fix: border color

* fix: padding and title line-height

* feat: add app-list card

* feat: use the AppListCard component

* nit: remove unused imports
2023-01-19 15:02:01 +00:00
sean-brydonandAlex van Andel cfaecc3e02 Icon small button variant (#6525)
* Icon small button variant

* Rename

* Update CVA + Update button WIP

* add ticket ref and fix padding

* Update button and all uses of Icon button

* SB deployment test

* Fixing type error

* Fix lint

* Add icon button to canvas

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-01-19 14:55:32 +00:00
Syed Ali Shahbaz 18246e0dc1 path.join causing issues on windows so we keep path like the other exports (#6568) 2023-01-19 13:21:51 +00:00
sean-brydon 6dcb16724c Fixing tabs again (#6566) 2023-01-19 12:05:00 +00:00
d4c5a906b5 App Store Templates (#5289)
* Start by moving what we have to _templates

* WIP

* WIP

* Add create/edit/delete template commands

* Type fixes cli

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-01-18 15:30:25 -07:00
Alex van Andel 27e8d73b82 Revert "experiment: disco border (#6531)"
This reverts commit b5f274d663.
2023-01-18 19:29:36 +00:00
Alex van Andel 25eae4a546 find indexes instead of just one (#6547) 2023-01-18 12:26:07 -07:00
Udit Takkar f663774fee feat: add most popular section in apps (#6539)
* feat: add most popular section in apps

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

* chore: remove margin

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

* fix: 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-18 11:48:10 -07:00
Nafees NazikandLeo Giovanetti e2258fba27 Feat: webhook form UX improvement (#6448)
* feat: add type and required attribute

* feat: disable ping button when subscriberUrl is
not present

Co-authored-by: Leo Giovanetti <hello@leog.me>
2023-01-18 16:02:20 +00:00
Carina WollendorferandCarinaWolli d0d8878f34 Adds ability to add custom sender names for workflow emails (#6463)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-01-18 15:32:39 +01:00
Udit Takkar 59ff6c35ee fix: limits tab in event types (#6545)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-18 12:58:13 +01:00
Alex van Andel 3c07e305ce Create local next.config.js, add storybook-static to .gitignore (#6544) 2023-01-18 11:54:03 +00:00
Alex van Andel b5f274d663 experiment: disco border (#6531)
* Had a play, looking good (now needs isLoading state or however we want to do that)

* Also add tailwind config..

* Don't show conic bg on lightmode

* Slow down animation, show on light mode

* Apply disco border both on light/dark mode & only on success page

* 5s->8s animation speed, remove border crisping up the corners
2023-01-18 10:40:06 +00:00
Peer Richelsen 6c3ae984fc add tracking URL to LicenseRequired (#6526) 2023-01-17 21:49:40 +00:00
Mohammed Cherfaoui a2e9bf0504 fix toast dimensions issues (#6522) 2023-01-18 02:45:04 +05:30
GitStart-Cal.comandgitstart-calcom 8aa089efe6 [CAL-760] Emails: Design Tokens are not update to 2.0 (#6530)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-01-17 22:14:16 +01:00
sean-brydonandzomars de64d3d9d4 Move exports up a folder (#6519)
* Move exports up a folder

* Fix imports

* Fix imports again

Co-authored-by: zomars <zomars@me.com>
2023-01-17 12:10:47 -07:00
Udit Takkar 71833c0737 fix: type error and border (#6520)
* fix: type error and border

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

* fix: sepeator

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

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-17 17:12:22 +00:00
Udit Takkar c78f4deaf7 fix: use suffix in text field (#6532)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-01-17 18:08:00 +01:00
Hariom Balhara 24fc7d611f Add routing-forms README (#6506) 2023-01-17 17:58:10 +01:00
sean-brydonandHariom Balhara 8afbc3b457 Fixing horizontal tabs (#6454)
* Fix spacing and height

* Testing if its timing out or something else

* So, timep=out is the problem. Increase the timeout to 1s for now

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-01-17 16:36:06 +00:00
sean-brydon da18aae21c Adding rtl support for SB (#6523) 2023-01-17 13:38:14 +01:00
Nafees Nazik 87a0d533ef fix: german translation (#6512) 2023-01-17 13:35:35 +01:00
Nafees Nazik 7cdd453953 fix: button shifting while focusing (#6521)
* fix: remove border on focus

* refactor: remove dark mode styles
2023-01-17 12:21:05 +00:00
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
510 changed files with 5951 additions and 5127 deletions

No files matched your search

+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'
@@ -79,6 +82,7 @@ SEND_FEEDBACK_EMAIL=
# Used for email reminders in workflows and internal sync services # Used for email reminders in workflows and internal sync services
SENDGRID_API_KEY= SENDGRID_API_KEY=
SENDGRID_EMAIL= SENDGRID_EMAIL=
NEXT_PUBLIC_SENDGRID_SENDER_NAME=
# Twilio # Twilio
# Used to send SMS reminders in workflows # Used to send SMS reminders in workflows
@@ -86,6 +90,7 @@ TWILIO_SID=
TWILIO_TOKEN= TWILIO_TOKEN=
TWILIO_MESSAGING_SID= TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER= TWILIO_PHONE_NUMBER=
# For NEXT_PUBLIC_SENDER_ID only letters, numbers and spaces are allowed (max. 11 characters)
NEXT_PUBLIC_SENDER_ID= NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID= TWILIO_VERIFY_SID=
@@ -149,3 +154,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
+4 -2
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,8 +23,8 @@ jobs:
with: with:
upload_translations: true upload_translations: true
download_translations: true download_translations: true
pull_request_labels: "♻️ autoupdate, automerge"
env: env:
# So required checks run on Crowdin PRs GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+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
+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
+6
View File
@@ -80,3 +80,9 @@ apps/storybook/build-storybook.log
# Snaplet # Snaplet
.snaplet/snapshots .snaplet/snapshots
.snaplet/structure.d.ts .snaplet/structure.d.ts
# Submodules
.gitmodules
apps/api
apps/website
apps/console
-12
View File
@@ -1,12 +0,0 @@
[submodule "apps/console"]
path = apps/console
url = https://github.com/calcom/console.git
branch = main
[submodule "apps/api"]
path = apps/api
url = https://github.com/calcom/api.git
branch = main
[submodule "apps/website"]
path = apps/website
url = https://github.com/calcom/website.git
branch = main
+7 -11
View File
@@ -305,6 +305,12 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases. See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases.
<!-- RORADMAP -->
## Repo Activity
<img width="100%" src="https://repobeats.axiom.co/api/embed/6bfca2f20f39738048b6e70ca205efde46352c3d.svg" />
<!-- CONTRIBUTING --> <!-- CONTRIBUTING -->
## Contributing ## Contributing
@@ -405,17 +411,6 @@ following
9. Click the "Save" button at the bottom footer. 9. Click the "Save" button at the bottom footer.
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts. 10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
### Obtaining Vital API Keys
1. Open [Vital](https://tryvital.io/) and click Get API Keys.
1. Create a team with the team name you desire
1. Head to the configuration section on the sidebar of the dashboard
1. Click on API keys and you'll find your sandbox `api_key`.
1. Copy your `api_key` to `VITAL_API_KEY` in the .env.appStore file.
1. Open [Vital Webhooks](https://app.tryvital.io/team/{team_id}/webhooks) and add `<CALCOM BASE URL>/api/integrations/vital/webhook` as webhook for connected applications.
1. Select all events for the webhook you interested, e.g. `sleep_created`
1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file.
## Workflows ## Workflows
### Setting up SendGrid for Email reminders ### Setting up SendGrid for Email reminders
@@ -425,6 +420,7 @@ following
3. Copy API key to your .env file into the SENDGRID_API_KEY field 3. Copy API key to your .env file into the SENDGRID_API_KEY field
4. Go to Settings -> Sender Authentication and verify a single sender 4. Go to Settings -> Sender Authentication and verify a single sender
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field 5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
6. Add your custom sender name to the .env file into the NEXT_PUBLIC_SENDGRID_SENDER_NAME field (fallback is Cal.com)
### Setting up Twilio for SMS reminders ### Setting up Twilio for SMS reminders
Submodule apps/api deleted from 7aebdb8c96.
Submodule apps/console deleted from 8dd0779c43.
+2 -1
View File
@@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
storybook-static
dist dist
dist-ssr dist-ssr
*.local *.local
@@ -21,4 +22,4 @@ dist-ssr
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
+4 -2
View File
@@ -11,13 +11,15 @@ module.exports = {
"@storybook/addon-links", "@storybook/addon-links",
"@storybook/addon-essentials", "@storybook/addon-essentials",
"@storybook/addon-interactions", "@storybook/addon-interactions",
"storybook-addon-rtl-direction",
"storybook-react-i18next", "storybook-react-i18next",
{ "storybook-addon-next",
/*{
name: "storybook-addon-next", name: "storybook-addon-next",
options: { options: {
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"), nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
}, },
}, },*/
], ],
framework: "@storybook/react", framework: "@storybook/react",
core: { core: {
+45
View File
@@ -0,0 +1,45 @@
const withBundleAnalyzer = require("@next/bundle-analyzer");
const withTM = require("next-transpile-modules")([
"@calcom/app-store",
"@calcom/dayjs",
"@calcom/emails",
"@calcom/trpc",
"@calcom/embed-core",
"@calcom/embed-react",
"@calcom/features",
"@calcom/lib",
"@calcom/prisma",
"@calcom/ui",
]);
const glob = require("glob");
const plugins = [];
plugins.push(withTM, withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
/** @type {import("next").NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ["www.datocms-assets.com"],
formats: ["image/avif", "image/webp"],
},
typescript: {
ignoreBuildErrors: true,
},
experimental: { images: { allowFutureImage: true } },
eslint: {
ignoreDuringBuilds: true,
},
webpack: (config, { isServer }) => {
if (!isServer) {
// don't resolve 'fs' module on the client to prevent this error on build --> Error: Can't resolve 'fs'
config.resolve.fallback = {
fs: false,
};
}
return config;
},
};
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
+2 -1
View File
@@ -22,7 +22,8 @@
"@radix-ui/react-tooltip": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0",
"next": "^13.1.1", "next": "^13.1.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"storybook-addon-rtl-direction": "^0.0.19"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19.6", "@babel/core": "^7.19.6",
+35
View File
@@ -0,0 +1,35 @@
import { ReactNode } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, ListItemText } from "@calcom/ui";
interface AppListCardProps {
logo?: string;
title: string;
description: string;
actions?: ReactNode;
isDefault?: boolean;
}
export default function AppListCard(props: AppListCardProps) {
const { t } = useLocale();
const { logo, title, description, actions, isDefault } = props;
return (
<div className="p-4">
<div className="flex items-center gap-x-3">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
<div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2">
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
{isDefault ? <Badge variant="green">{t("default")}</Badge> : null}
</div>
<ListItemText component="p">{description}</ListItemText>
</div>
{actions}
</div>
</div>
);
}
+10 -10
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>}
@@ -597,7 +597,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
<iframe <iframe
ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>} ref={ref as typeof ref & MutableRefObject<HTMLIFrameElement>}
data-testid="embed-preview" data-testid="embed-preview"
className="border-1 h-[100vh] border" className="h-[100vh] border"
width="100%" width="100%"
height="100%" height="100%"
src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`} src={`${WEBAPP_URL}/embed/preview.html?embedType=${embedType}&calLink=${calLink}`}
@@ -639,7 +639,7 @@ const ChooseEmbedTypesDialogContent = () => {
<div className="flex items-start"> <div className="flex items-start">
{embeds.map((embed, index) => ( {embeds.map((embed, index) => (
<button <button
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100 ltr:mr-2 rtl:ml-2" className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
key={index} key={index}
data-testid={embed.type} data-testid={embed.type}
onClick={() => { onClick={() => {
@@ -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>
@@ -1163,7 +1163,7 @@ export const EmbedButton = <T extends React.ElementType>({
...props ...props
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => { }: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
const router = useRouter(); const router = useRouter();
className = classNames(className, "hidden lg:inline-flex"); className = classNames("hidden lg:inline-flex", className);
const openEmbedModal = () => { const openEmbedModal = () => {
goto(router, { goto(router, {
dialog: "embed", dialog: "embed",
+1 -1
View File
@@ -142,7 +142,7 @@ export default function ImageUploader({
</div> </div>
)} )}
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />} {result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900"> <label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
<input <input
onInput={onInputFile} onInput={onInputFile}
type="file" type="file"
+2 -2
View File
@@ -67,7 +67,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
onClick={onClick} onClick={onClick}
className={classNames( className={classNames(
isCurrent isCurrent
? "border-neutral-900 text-neutral-900" ? "border-gray-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";
+9 -1
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, Badge } from "@calcom/ui";
const Component = ({ const Component = ({
name, name,
@@ -33,6 +34,7 @@ const Component = ({
privacy, privacy,
isProOnly, isProOnly,
images, images,
isTemplate,
}: Parameters<typeof App>[0]) => { }: Parameters<typeof App>[0]) => {
const { t } = useLocale(); const { t } = useLocale();
const hasImages = images && images.length > 0; const hasImages = images && images.length > 0;
@@ -105,6 +107,11 @@ const Component = ({
</Link>{" "} </Link>{" "}
• {t("published_by", { author })} • {t("published_by", { author })}
</h2> </h2>
{isTemplate && (
<Badge variant="red" className="mt-4">
Template - Available in Dev Environment only for testing
</Badge>
)}
</header> </header>
</div> </div>
{!appCredentials.isLoading ? ( {!appCredentials.isLoading ? (
@@ -309,6 +316,7 @@ export default function App(props: {
licenseRequired: AppType["licenseRequired"]; licenseRequired: AppType["licenseRequired"];
isProOnly: AppType["isProOnly"]; isProOnly: AppType["isProOnly"];
images?: string[]; images?: string[];
isTemplate?: boolean;
}) { }) {
const { t } = useLocale(); const { t } = useLocale();
@@ -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
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { ReactNode, useEffect, useState } from "react"; import { ReactNode, useEffect, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui"; import { Badge, Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
import classNames from "@lib/classNames"; import classNames from "@lib/classNames";
@@ -19,6 +19,7 @@ function IntegrationListItem(props: {
destination?: boolean; destination?: boolean;
separate?: boolean; separate?: boolean;
invalidCredential?: boolean; invalidCredential?: boolean;
isTemplate?: boolean;
}): JSX.Element { }): JSX.Element {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
@@ -50,8 +51,13 @@ function IntegrationListItem(props: {
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}> <div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
{props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />} {props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />}
<div className="flex-grow truncate pl-2"> <div className="flex-grow truncate pl-2">
<ListItemTitle component="h3"> <ListItemTitle component="h3" className="flex ">
<Link href={"/apps/" + props.slug}>{props.name || title}</Link> <Link href={"/apps/" + props.slug}>{props.name || title}</Link>
{props.isTemplate && (
<Badge variant="red" className="ml-4">
Template
</Badge>
)}
</ListItemTitle> </ListItemTitle>
<ListItemText component="p">{props.description}</ListItemText> <ListItemText component="p">{props.description}</ListItemText>
{/* Alert error that key stopped working. */} {/* Alert error that key stopped working. */}
@@ -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";
@@ -6,7 +6,7 @@ import classNames from "@lib/classNames";
function SkeletonLoader() { function SkeletonLoader() {
return ( return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden"> <ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
@@ -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 } : {}),
}, },
}; };
@@ -119,7 +119,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
prefetch={false} prefetch={false}
className={classNames( className={classNames(
"text-primary-500 hover:border-gray-900 hover:bg-gray-50", "text-primary-500 hover:border-gray-900 hover:bg-gray-50",
"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", "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" ? "" : "" brand === "#fff" || brand === "#ffffff" ? "" : ""
)} )}
data-testid="time"> data-testid="time">
@@ -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">
@@ -220,7 +220,8 @@ function BookingListItem(booking: BookingItemProps) {
}, },
}); });
}; };
const showRecordingsButtons = booking.location === "integrations:daily" && isPast && isConfirmed; const showRecordingsButtons =
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
return ( return (
<> <>
<RescheduleDialog <RescheduleDialog
@@ -273,7 +274,7 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row"> <tr className="group flex flex-col hover:bg-gray-50 sm:flex-row">
<td <td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]" className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}> onClick={onClickTableData}>
@@ -356,7 +357,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 ">
@@ -4,7 +4,7 @@ import { SkeletonText } from "@calcom/ui";
function SkeletonLoader() { function SkeletonLoader() {
return ( return (
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden"> <ul className="animate-pulse divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
+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}
/> />
); );
@@ -492,7 +492,7 @@ const BookingPage = ({
<div <div
className={classNames( className={classNames(
"main overflow-hidden", "main overflow-hidden",
isBackgroundTransparent ? "" : "dark:border-1 dark:bg-darkgray-100 bg-white", isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-white dark:border",
"dark:border-darkgray-300 rounded-md sm:border" "dark:border-darkgray-300 rounded-md sm:border"
)}> )}>
<div className="sm:flex"> <div className="sm:flex">
@@ -954,19 +954,14 @@ const BookingPage = ({
<Button <Button
type="button" type="button"
color="minimal" color="minimal"
size="icon" variant="icon"
tooltip={t("additional_guests")} tooltip={t("additional_guests")}
StartIcon={Icon.FiUserPlus} StartIcon={Icon.FiUserPlus}
onClick={() => setGuestToggle(!guestToggle)} onClick={() => setGuestToggle(!guestToggle)}
className="mr-auto" className="mr-auto"
/> />
)} )}
<Button <Button color="minimal" type="button" onClick={() => router.back()}>
color="minimal"
type="button"
onClick={() => router.back()}
// We override this for this component only for now - as we don't support darkmode everywhere in the app
className="dark:hover:bg-darkgray-200 dark:border-none dark:text-white">
{t("cancel")} {t("cancel")}
</Button> </Button>
<Button <Button
@@ -3,7 +3,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js"; import { isValidPhoneNumber } from "libphonenumber-js";
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form"; import { Controller, useForm, useWatch } from "react-hook-form";
import { components } from "react-select";
import { z } from "zod"; import { z } from "zod";
import { import {
@@ -14,32 +13,25 @@ import {
LocationObject, LocationObject,
LocationType, LocationType,
} from "@calcom/app-store/locations"; } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
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 { Button, Dialog, DialogClose, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui"; import { Button, Dialog, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell"; import { QueryCell } from "@lib/QueryCell";
import CheckboxField from "@components/ui/form/CheckboxField"; import CheckboxField from "@components/ui/form/CheckboxField";
import Select from "@components/ui/form/Select"; import LocationSelect, { LocationOption } from "@components/ui/form/LocationSelect";
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number]; type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
type OptionTypeBase = {
label: string;
value: EventLocationType["type"];
disabled?: boolean;
};
interface ISetLocationDialog { interface ISetLocationDialog {
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void; saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: OptionTypeBase; selection?: LocationOption;
booking?: BookingItem; booking?: BookingItem;
defaultValues?: LocationObject[]; defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>; setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean; isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void; setSelectedLocation?: (param: LocationOption | undefined) => void;
setEditingLocationType?: (param: string) => void; setEditingLocationType?: (param: string) => void;
} }
@@ -211,8 +203,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
})(); })();
return ( return (
<Dialog open={isOpenDialog}> <Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
<DialogContent> <DialogContent disableOverflow>
<div className="flex flex-row space-x-3"> <div className="flex flex-row space-x-3">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10"> <div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
<Icon.FiMapPin className="text-primary-600 h-6 w-6" /> <Icon.FiMapPin className="text-primary-600 h-6 w-6" />
@@ -296,38 +288,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
name="locationType" name="locationType"
control={locationFormMethods.control} control={locationFormMethods.control}
render={() => ( render={() => (
<Select<{ label: string; value: string; icon?: string }> <LocationSelect
maxMenuHeight={300} maxMenuHeight={300}
name="location" name="location"
defaultValue={selection} defaultValue={selection}
options={locationOptions} options={locationOptions}
components={{
Option: (props) => (
<components.Option {...props}>
<div className="flex items-center gap-3">
{props.data.icon && (
<img src={props.data.icon} alt="cover" className="h-3.5 w-3.5" />
)}
<span
className={classNames(
"text-sm font-medium",
props.isSelected ? "text-white" : "text-gray-900"
)}>
{props.data.label}
</span>
</div>
</components.Option>
),
}}
formatOptionLabel={(e) => (
<div className="flex items-center gap-3">
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
<span>{e.label}</span>
</div>
)}
formatGroupLabel={(e) => (
<p className="text-xs font-medium text-gray-600">{e.label}</p>
)}
isSearchable isSearchable
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm" className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm"
onChange={(val) => { onChange={(val) => {
@@ -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>
@@ -183,7 +183,7 @@ function RadioInputHandler({
{...register(`options.${index}.label` as const, { required: true })} {...register(`options.${index}.label` as const, { required: true })}
addOnSuffix={ addOnSuffix={
<Button <Button
size="icon" variant="icon"
color="minimal" color="minimal"
StartIcon={Icon.FiX} StartIcon={Icon.FiX}
onClick={() => { onClick={() => {
@@ -129,7 +129,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Button <Button
type="button" type="button"
StartIcon={Icon.FiEdit} StartIcon={Icon.FiEdit}
size="icon" variant="icon"
color="minimal" color="minimal"
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black" className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
onClick={() => setShowEventNameTip((old) => !old)} onClick={() => setShowEventNameTip((old) => !old)}
+7 -33
View File
@@ -1,43 +1,17 @@
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]"; import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext"; import { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated"; import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
import { EventTypeAppCardComponentProps } from "@calcom/app-store/types"; import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
import { EventTypeAppsList } from "@calcom/app-store/utils"; import { EventTypeAppsList } from "@calcom/app-store/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui"; import { Button, EmptyScreen, Icon } from "@calcom/ui";
type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] & export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"]; EventTypeAppCardComponentProps["eventType"];
function AppCardWrapper({
app,
eventType,
getAppData,
setAppData,
}: {
app: RouterOutputs["viewer"]["apps"][number];
eventType: EventType;
getAppData: GetAppData;
setAppData: SetAppData;
}) {
const dirName = app.slug === "stripe" ? "stripepayment" : app.slug;
const Component = EventTypeAddonMap[dirName as keyof typeof EventTypeAddonMap];
if (!Component) {
throw new Error('No component found for "' + dirName + '"');
}
return (
<ErrorBoundary message={`There is some problem with ${app.name} App`}>
<EventTypeAppContext.Provider value={[getAppData, setAppData]}>
<Component key={app.slug} app={app} eventType={eventType} />
</EventTypeAppContext.Provider>
</ErrorBoundary>
);
}
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => { export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const { t } = useLocale(); const { t } = useLocale();
const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({ const { data: eventTypeApps, isLoading } = trpc.viewer.apps.useQuery({
@@ -97,7 +71,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
/> />
) : null} ) : null}
{installedApps?.map((app) => ( {installedApps?.map((app) => (
<AppCardWrapper <EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)} getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)} setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug} key={app.slug}
@@ -113,7 +87,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
) : null} ) : null}
<div className="before:border-0"> <div className="before:border-0">
{notInstalledApps?.map((app) => ( {notInstalledApps?.map((app) => (
<AppCardWrapper <EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)} getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)} setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
key={app.slug} key={app.slug}
+192 -208
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,212 +131,115 @@ 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="space-y-4 lg:space-y-8">
<div className="w-full"> <div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label> <div className="w-full">
<Controller <Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
name="beforeBufferTime" <Controller
control={formMethods.control} name="beforeBufferTime"
defaultValue={eventType.beforeEventBuffer || 0} control={formMethods.control}
render={({ field: { onChange, value } }) => { defaultValue={eventType.beforeEventBuffer || 0}
const beforeBufferOptions = [ render={({ field: { onChange, value } }) => {
{ const beforeBufferOptions = [
label: t("event_buffer_default"), {
value: 0, label: t("event_buffer_default"),
}, value: 0,
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({ },
label: minutes + " " + t("minutes"), ...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
value: minutes, label: minutes + " " + t("minutes"),
})), value: minutes,
]; })),
return ( ];
<Select return (
isSearchable={false}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
}
options={beforeBufferOptions}
/>
);
}}
/>
</div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
}
options={afterBufferOptions}
/>
);
}}
/>
</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 w-full items-end justify-end">
<Controller
name="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 <Select
isSearchable={false} isSearchable={false}
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]" onChange={(val) => {
defaultValue={durationTypeOptions.find( if (val) onChange(val.value);
(option) => option.value === minimumBookingNoticeType.current }}
)} defaultValue={
onChange={(input) => onMinimumNoticeDurationTypeChange(input?.value as DurationType)} beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
options={durationTypeOptions} }
options={beforeBufferOptions}
/> />
</> );
); }}
}} />
/> </div>
<div className="w-full">
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
label: t("event_buffer_default"),
value: 0,
},
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"),
value: minutes,
})),
];
return (
<Select
isSearchable={false}
onChange={(val) => {
if (val) onChange(val.value);
}}
defaultValue={
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
}
options={afterBufferOptions}
/>
);
}}
/>
</div>
</div> </div>
<div className="w-full"> <div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
<Label htmlFor="slotInterval">{t("slot_interval")} </Label> <div className="w-full">
<Controller <Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
name="slotInterval" <MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
control={formMethods.control} </div>
render={() => { <div className="w-full">
const slotIntervalOptions = [ <Label htmlFor="slotInterval">{t("slot_interval")} </Label>
{ <Controller
label: t("slot_interval_default"), name="slotInterval"
value: -1, control={formMethods.control}
}, render={() => {
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({ const slotIntervalOptions = [
label: minutes + " " + t("minutes"), {
value: minutes, label: t("slot_interval_default"),
})), value: -1,
]; },
return ( ...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
<Select label: minutes + " " + t("minutes"),
isSearchable={false} value: minutes,
onChange={(val) => { })),
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null); ];
}} return (
defaultValue={ <Select
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) || isSearchable={false}
slotIntervalOptions[0] onChange={(val) => {
} formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
options={slotIntervalOptions} }}
/> defaultValue={
); slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
}} slotIntervalOptions[0]
/> }
options={slotIntervalOptions}
/>
);
}}
/>
</div>
</div> </div>
</div> </div>
<hr />
<hr className="my-8" />
<Controller <Controller
name="bookingLimits" name="bookingLimits"
control={formMethods.control} control={formMethods.control}
@@ -276,8 +261,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}
@@ -446,7 +430,7 @@ const BookingLimits = () => {
}} }}
/> />
<Button <Button
size="icon" variant="icon"
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
color="destructive" color="destructive"
onClick={() => { onClick={() => {
+24 -11
View File
@@ -17,11 +17,21 @@ import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from
import { slugify } from "@lib/slugify"; import { slugify } from "@lib/slugify";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import LocationSelect, {
SingleValueLocationOption,
LocationOption,
} from "@components/ui/form/LocationSelect";
type OptionTypeBase = { const getLocationFromType = (
label: string; type: EventLocationType["type"],
value: EventLocationType["type"]; locationOptions: Pick<EventTypeSetupProps, "locationOptions">["locationOptions"]
disabled?: boolean; ) => {
for (const locationOption of locationOptions) {
const option = locationOption.options.find((option) => option.value === type);
if (option) {
return option;
}
}
}; };
export const EventSetupTab = ( export const EventSetupTab = (
@@ -32,7 +42,7 @@ export const EventSetupTab = (
const { eventType, locationOptions, team } = props; const { eventType, locationOptions, team } = props;
const [showLocationModal, setShowLocationModal] = useState(false); const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>(""); const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined); const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration); const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({ const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
@@ -51,7 +61,8 @@ export const EventSetupTab = (
); );
const openLocationModal = (type: EventLocationType["type"]) => { const openLocationModal = (type: EventLocationType["type"]) => {
setSelectedLocation(locationOptions.find((option) => option.value === type)); const option = getLocationFromType(type, locationOptions);
setSelectedLocation(option);
setShowLocationModal(true); setShowLocationModal(true);
}; };
@@ -131,14 +142,14 @@ export const EventSetupTab = (
<div className="w-full"> <div className="w-full">
{validLocations.length === 0 && ( {validLocations.length === 0 && (
<div className="flex"> <div className="flex">
<Select <LocationSelect
placeholder={t("select")} placeholder={t("select")}
options={locationOptions} options={locationOptions}
isSearchable={false} isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm text-sm" className="block w-full min-w-0 flex-1 rounded-sm text-sm"
onChange={(e) => { onChange={(e: SingleValueLocationOption) => {
if (e?.value) { if (e?.value) {
const newLocationType: EventLocationType["type"] = e.value; const newLocationType = e.value;
const eventLocationType = getEventLocationType(newLocationType); const eventLocationType = getEventLocationType(newLocationType);
if (!eventLocationType) { if (!eventLocationType) {
return; return;
@@ -162,7 +173,7 @@ export const EventSetupTab = (
return null; return null;
} }
return ( return (
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 py-1.5 px-2"> <li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
<div className="flex max-w-full justify-between"> <div className="flex max-w-full justify-between">
<div key={index} className="flex flex-grow items-center"> <div key={index} className="flex flex-grow items-center">
<img <img
@@ -362,7 +373,9 @@ export const EventSetupTab = (
saveLocation={saveLocation} saveLocation={saveLocation}
defaultValues={formMethods.getValues("locations")} defaultValues={formMethods.getValues("locations")}
selection={ selection={
selectedLocation ? { value: selectedLocation.value, label: t(selectedLocation.label) } : undefined selectedLocation
? { value: selectedLocation.value, label: t(selectedLocation.label), icon: selectedLocation.icon }
: undefined
} }
setSelectedLocation={setSelectedLocation} setSelectedLocation={setSelectedLocation}
setEditingLocationType={setEditingLocationType} setEditingLocationType={setEditingLocationType}
+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>
@@ -5,6 +5,7 @@ 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,
@@ -55,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",
@@ -151,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",
@@ -204,7 +208,7 @@ function EventTypeSingleLayout({
<Button <Button
color="secondary" color="secondary"
target="_blank" target="_blank"
size="icon" variant="icon"
href={permalink} href={permalink}
rel="noreferrer" rel="noreferrer"
StartIcon={Icon.FiExternalLink} StartIcon={Icon.FiExternalLink}
@@ -213,7 +217,7 @@ function EventTypeSingleLayout({
<Button <Button
color="secondary" color="secondary"
size="icon" variant="icon"
StartIcon={Icon.FiLink} StartIcon={Icon.FiLink}
tooltip={t("copy_link")} tooltip={t("copy_link")}
onClick={() => { onClick={() => {
@@ -225,12 +229,12 @@ function EventTypeSingleLayout({
embedUrl={encodeURIComponent(embedLink)} embedUrl={encodeURIComponent(embedLink)}
StartIcon={Icon.FiCode} StartIcon={Icon.FiCode}
color="secondary" color="secondary"
size="icon" variant="icon"
tooltip={t("embed")} tooltip={t("embed")}
/> />
<Button <Button
color="secondary" color="secondary"
size="icon" variant="icon"
StartIcon={Icon.FiTrash} StartIcon={Icon.FiTrash}
tooltip={t("delete")} tooltip={t("delete")}
disabled={!hasPermsToDelete} disabled={!hasPermsToDelete}
@@ -241,37 +245,46 @@ function EventTypeSingleLayout({
<VerticalDivider className="hidden lg:block" /> <VerticalDivider className="hidden lg:block" />
<Dropdown> <Dropdown>
<DropdownMenuTrigger className="block h-9 w-9 justify-center rounded-md border border-gray-200 bg-transparent text-gray-700 lg:hidden"> <DropdownMenuTrigger asChild>
<Icon.FiMoreHorizontal className="group-hover:text-gray-800" /> <Button
className="lg:hidden"
StartIcon={Icon.FiMoreHorizontal}
variant="icon"
color="secondary"
/>
</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}
@@ -316,7 +329,7 @@ function EventTypeSingleLayout({
<div className="w-full ltr:mr-2 rtl:ml-2"> <div className="w-full ltr:mr-2 rtl:ml-2">
<div <div
className={classNames( className={classNames(
"mt-4 rounded-md border-neutral-200 bg-white sm:mx-0 xl:mt-0", "mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6" disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
)}> )}>
{children} {children}
@@ -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,
})} })}
@@ -10,7 +10,7 @@ function SkeletonLoader() {
<SkeletonText className="h-4 w-24" /> <SkeletonText className="h-4 w-24" />
</div> </div>
</div> </div>
<ul className="divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden"> <ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
<SkeletonItem /> <SkeletonItem />
@@ -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">
@@ -113,7 +113,7 @@ const UserProfile = (props: IUserProfileProps) => {
name="avatar" name="avatar"
id="avatar" id="avatar"
placeholder="URL" placeholder="URL"
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-neutral-800 focus:outline-none focus:ring-neutral-800" className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm focus:border-gray-800 focus:outline-none focus:ring-gray-800"
defaultValue={imageSrc} defaultValue={imageSrc}
/> />
<div className="flex items-center px-4"> <div className="flex items-center px-4">
@@ -146,7 +146,7 @@ const UserProfile = (props: IUserProfileProps) => {
ref={bioRef} ref={bioRef}
name="bio" name="bio"
id="bio" id="bio"
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm" className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm"
defaultValue={user?.bio || undefined} defaultValue={user?.bio || undefined}
onChange={(event) => { onChange={(event) => {
setValue("bio", event.target.value); setValue("bio", event.target.value);
@@ -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>
+3 -3
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">
@@ -32,10 +32,10 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
</div> </div>
)} )}
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md"> <div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10"> <div className="mx-2 rounded-md border 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>
-51
View File
@@ -1,51 +0,0 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as Tooltip from "@radix-ui/react-tooltip";
import { Maybe } from "@calcom/trpc/server";
import classNames from "@lib/classNames";
import { defaultAvatarSrc } from "@lib/profile";
export type AvatarProps = {
className?: string;
size?: number;
imageSrc?: Maybe<string>;
title?: string;
alt: string;
gravatarFallbackMd5?: string;
};
/**
* @deprecated Use AvatarSSR instead. Once, there is no usage of Avatar, AvatarSSR can be renamed.
*/
export default function Avatar(props: AvatarProps) {
const { imageSrc, gravatarFallbackMd5, size, alt, title } = props;
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
const avatar = (
<AvatarPrimitive.Root>
<AvatarPrimitive.Image
src={imageSrc ?? undefined}
alt={alt}
className={classNames("rounded-full", `h-auto w-${size}`, props.className)}
/>
<AvatarPrimitive.Fallback delayMs={600}>
{gravatarFallbackMd5 && (
// eslint-disable-next-line @next/next/no-img-element
<img src={defaultAvatarSrc({ md5: gravatarFallbackMd5 })} alt={alt} className={className} />
)}
</AvatarPrimitive.Fallback>
</AvatarPrimitive.Root>
);
return title ? (
<Tooltip.Tooltip delayDuration={300}>
<Tooltip.TooltipTrigger className="cursor-default">{avatar}</Tooltip.TooltipTrigger>
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white">
<Tooltip.Arrow />
{title}
</Tooltip.Content>
</Tooltip.Tooltip>
) : (
<>{avatar}</>
);
}
-47
View File
@@ -1,47 +0,0 @@
import Link from "next/link";
import React from "react";
import classNames from "@lib/classNames";
import { AvatarSSR } from "@components/ui/AvatarSSR";
export type AvatarGroupProps = {
border?: string; // this needs to be the color of the parent container background, i.e.: border-white dark:border-gray-900
size: number;
truncateAfter?: number;
items: {
image: string;
title?: string;
alt?: string;
href?: string;
}[];
className?: string;
};
export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
return (
<ul className={classNames(props.className)}>
{props.items.slice(0, props.truncateAfter).map((item, idx) => {
if (item.image != null) {
const avatar = (
<AvatarSSR
className={props.border}
imageSrc={item.image}
title={item.title}
alt={item.alt || ""}
size={props.size}
/>
);
return (
<li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2">
{item.href ? <Link href={item.href}>{avatar}</Link> : avatar}
</li>
);
}
})}
</ul>
);
};
export default AvatarGroup;
-62
View File
@@ -1,62 +0,0 @@
import { User } from "@prisma/client";
import * as Tooltip from "@radix-ui/react-tooltip";
import classNames from "@lib/classNames";
export type AvatarProps = (
| {
user: Pick<User, "name" | "username" | "avatar"> & { emailMd5?: string };
}
| {
user?: null;
imageSrc: string;
}
) & {
className?: string;
size?: number;
title?: string;
href?: string;
alt: string;
};
// defaultAvatarSrc from profile.tsx can't be used as it imports crypto
function defaultAvatarSrc(md5: string) {
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
}
// An SSR Supported version of Avatar component.
export function AvatarSSR(props: AvatarProps) {
const { size, title } = props;
let imgSrc = "";
let alt: string = props.alt;
if (props.user) {
const user = props.user;
const nameOrUsername = user.name || user.username || "";
alt = alt || nameOrUsername;
if (user.avatar) {
imgSrc = user.avatar;
} else if (user.emailMd5) {
imgSrc = defaultAvatarSrc(user.emailMd5);
}
} else {
imgSrc = props.imageSrc;
}
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
const avatar = imgSrc ? <img alt={alt} className={className} src={imgSrc} /> : null;
return title ? (
<Tooltip.Tooltip delayDuration={300}>
<Tooltip.TooltipTrigger asChild>{avatar}</Tooltip.TooltipTrigger>
<Tooltip.Content className="rounded-sm bg-black p-2 text-sm text-white">
<Tooltip.Arrow />
{title}
</Tooltip.Content>
</Tooltip.Tooltip>
) : (
<>{avatar}</>
);
}
+1 -1
View File
@@ -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>
@@ -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>
@@ -190,7 +190,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<div className="flex rounded-md"> <div className="flex rounded-md">
<span <span
className={classNames( className={classNames(
isInputUsernamePremium ? "border-1 border-orange-400 " : "", isInputUsernamePremium ? "border border-orange-400 " : "",
"hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex" "hidden h-9 items-center rounded-l-md border border-r-0 border-gray-300 border-r-gray-300 bg-gray-50 px-3 text-sm text-gray-500 md:inline-flex"
)}> )}>
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/ {process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
@@ -207,8 +207,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
className={classNames( className={classNames(
"border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0", "border-l-1 mb-0 mt-0 rounded-md rounded-l-none font-sans text-sm leading-4 focus:!ring-0",
isInputUsernamePremium isInputUsernamePremium
? "border-1 focus:border-1 border-orange-400 focus:border-orange-400" ? "border border-orange-400 focus:border focus:border-orange-400"
: "border-1 focus:border-2", : "border focus:border",
markAsError markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none" ? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
: "border-l-gray-300", : "border-l-gray-300",
@@ -131,7 +131,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
autoCapitalize="none" autoCapitalize="none"
autoCorrect="none" autoCorrect="none"
className={classNames( className={classNames(
"mb-0 mt-0 h-6 rounded-md ltr:rounded-l-none rtl:rounded-r-none", "mb-0 mt-0 rounded-md ltr:rounded-l-none rtl:rounded-r-none",
markAsError markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0" ? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
: "" : ""
@@ -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"
), ),
}, },
<> <>
@@ -2,9 +2,8 @@ import React from "react";
import { Props } from "react-select"; import { Props } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui"; import { Icon, Avatar } from "@calcom/ui";
import Avatar from "@components/ui/Avatar";
import Select from "@components/ui/form/Select"; import Select from "@components/ui/form/Select";
type CheckedSelectOption = { type CheckedSelectOption = {
@@ -41,16 +40,17 @@ export const CheckedSelect = ({
{...props} {...props}
/> />
{value.map((option) => ( {value.map((option) => (
<div key={option.value} className="border-1 border p-2 font-medium"> <div key={option.value} className="border p-2 font-medium">
<Avatar <Avatar
className="inline h-6 w-6 rounded-full ltr:mr-2 rtl:ml-2" className="inline ltr:mr-2 rtl:ml-2"
size="sm"
imageSrc={option.avatar} imageSrc={option.avatar}
alt={option.label} alt={option.label}
/> />
{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>
))} ))}
@@ -0,0 +1,64 @@
import { components, GroupBase, Props, SingleValue } from "react-select";
import type { EventLocationType } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import Select from "@components/ui/form/Select";
export type LocationOption = {
label: string;
value: EventLocationType["type"];
icon?: string;
disabled?: boolean;
};
export type SingleValueLocationOption = SingleValue<LocationOption>;
export type GroupOptionType = GroupBase<LocationOption>;
const OptionWithIcon = ({
icon,
isSelected,
label,
}: {
icon?: string;
isSelected?: boolean;
label: string;
}) => {
return (
<div className="flex items-center gap-3">
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5" />}
<span className={classNames("text-sm font-medium", isSelected ? "text-white" : "text-gray-900")}>
{label}
</span>
</div>
);
};
export default function LocationSelect(props: Props<LocationOption, false, GroupOptionType>) {
return (
<Select<LocationOption>
name="location"
components={{
Option: (props) => (
<components.Option {...props}>
<OptionWithIcon icon={props.data.icon} label={props.data.label} isSelected={props.isSelected} />
</components.Option>
),
SingleValue: (props) => (
<components.SingleValue {...props}>
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
</components.SingleValue>
),
}}
formatOptionLabel={(e) => (
<div className="flex items-center gap-3">
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
<span>{e.label}</span>
</div>
)}
formatGroupLabel={(e) => <p className="text-xs font-medium text-gray-600">{e.label}</p>}
{...props}
/>
);
}
+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
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) => {
+1 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "2.4.6", "version": "2.5.3",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -89,7 +89,6 @@
"next-seo": "^4.26.0", "next-seo": "^4.26.0",
"next-themes": "^0.2.0", "next-themes": "^0.2.0",
"next-transpile-modules": "^10.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",
+9 -14
View File
@@ -26,14 +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, Avatar } 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 { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) { export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
@@ -57,22 +54,21 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{eventTypes.map((type, index) => ( {eventTypes.map((type, index) => (
<li <li
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-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-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 <Link
href={getUsernameSlugLink({ users: props.users, slug: type.slug })} href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
className="flex justify-between px-6 py-4" className="flex justify-between px-6 py-4"
data-testid="event-type-link"> data-testid="event-type-link">
<div className="flex-shrink"> <div className="flex-shrink">
<p className="dark:text-darkgray-700 text-sm font-semibold text-neutral-900">{type.title}</p> <p className="dark:text-darkgray-700 text-sm font-semibold text-gray-900">{type.title}</p>
<EventTypeDescription className="text-sm" eventType={type} /> <EventTypeDescription className="text-sm" eventType={type} />
</div> </div>
<div className="mt-1 self-center"> <div className="mt-1 self-center">
<AvatarGroup <AvatarGroup
border="border-2 border-white dark:border-darkgray-100"
truncateAfter={4} truncateAfter={4}
className="flex flex-shrink-0" className="flex flex-shrink-0"
size={10} size="sm"
items={props.users.map((user) => ({ items={props.users.map((user) => ({
alt: user.name || "", alt: user.name || "",
image: user.avatar || "", image: user.avatar || "",
@@ -134,21 +130,20 @@ 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} /> <Avatar imageSrc={user.avatar} size="xl" 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
className={classNames( className={classNames(
"rounded-md ", "rounded-md ",
!isEventListEmpty && !isEventListEmpty && "border border-gray-200 dark:border-gray-700 dark:hover:border-gray-600"
"border border-neutral-200 dark:border-neutral-700 dark:hover:border-neutral-600"
)} )}
data-testid="event-types"> data-testid="event-types">
{user.away ? ( {user.away ? (
@@ -167,7 +162,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<div <div
key={type.id} key={type.id}
style={{ display: "flex", ...eventTypeListItemEmbedStyles }} style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
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-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
<Icon.FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" /> <Icon.FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */} {/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
<Link <Link
+1 -1
View File
@@ -24,7 +24,7 @@ export default function Type(props: AvailabilityPageProps) {
const { t } = useLocale(); const { t } = useLocale();
return props.away ? ( return props.away ? (
<div className="h-screen dark:bg-neutral-900"> <div className="h-screen dark:bg-gray-900">
<main className="mx-auto max-w-3xl px-4 py-24"> <main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types"> <div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900"> <div className="overflow-hidden rounded-sm border dark:border-gray-900">
+2 -2
View File
@@ -27,7 +27,7 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) { export default function Book(props: BookPageProps) {
const { t } = useLocale(); const { t } = useLocale();
return props.away ? ( return props.away ? (
<div className="h-screen dark:bg-neutral-900"> <div className="h-screen dark:bg-gray-900">
<main className="mx-auto max-w-3xl px-4 py-24"> <main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types"> <div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900"> <div className="overflow-hidden rounded-sm border dark:border-gray-900">
@@ -42,7 +42,7 @@ export default function Book(props: BookPageProps) {
</main> </main>
</div> </div>
) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? ( ) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
<div className="h-screen dark:bg-neutral-900"> <div className="h-screen dark:bg-gray-900">
<main className="mx-auto max-w-3xl px-4 py-24"> <main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types"> <div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900"> <div className="overflow-hidden rounded-sm border dark:border-gray-900">
+1 -1
View File
@@ -81,7 +81,7 @@ class MyDocument extends Document<Props> {
</Head> </Head>
<body <body
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100" className="dark:bg-darkgray-50 desktop-transparent bg-gray-100 antialiased"
style={ style={
this.props.isEmbed this.props.isEmbed
? { ? {
+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,
+16 -5
View File
@@ -35,6 +35,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
licenseRequired={data.licenseRequired} licenseRequired={data.licenseRequired}
isProOnly={data.isProOnly} isProOnly={data.isProOnly}
images={source.data?.items as string[] | undefined} images={source.data?.items as string[] | undefined}
isTemplate={data.isTemplate}
// tos="https://zoom.us/terms" // tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy" // privacy="https://zoom.us/privacy"
body={ body={
@@ -53,7 +54,7 @@ export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
return { return {
paths, paths,
fallback: false, fallback: "blocking",
}; };
}; };
@@ -61,7 +62,7 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (typeof ctx.params?.slug !== "string") return { notFound: true }; if (typeof ctx.params?.slug !== "string") return { notFound: true };
const app = await prisma.app.findUnique({ const app = await prisma.app.findUnique({
where: { slug: ctx.params.slug }, where: { slug: ctx.params.slug.toLowerCase() },
}); });
if (!app) return { notFound: true }; if (!app) return { notFound: true };
@@ -70,21 +71,31 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (!singleApp) return { notFound: true }; if (!singleApp) return { notFound: true };
const appDirname = app.dirName; const isTemplate = singleApp.isTemplate;
const appDirname = path.join(isTemplate ? "templates" : "", app.dirName);
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`); 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 = "";
try { try {
/* If the app doesn't have a README we fallback to the package description */
source = fs.readFileSync(postFilePath).toString(); source = fs.readFileSync(postFilePath).toString();
source = source.replace(/{DESCRIPTION}/g, singleApp.description);
} catch (error) { } catch (error) {
/* If the app doesn't have a README we fallback to the package description */
console.log(`No DESCRIPTION.md 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);
if (data.items) {
data.items = data.items.map((item: string) => {
if (!item.includes("/api/app-store")) {
// Make relative paths absolute
return `/api/app-store/${appDirname}/${item}`;
}
return item;
});
}
return { return {
props: { props: {
source: { content, data }, source: { content, data },
@@ -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();
+2 -1
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();
+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,
PopularAppsSlider,
} 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} />
</> <PopularAppsSlider 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 {
@@ -115,6 +115,7 @@ const IntegrationsList = ({ data }: IntegrationsListProps) => {
logo={item.logo} logo={item.logo}
description={item.description} description={item.description}
separate={true} separate={true}
isTemplate={item.isTemplate}
invalidCredential={item.invalidCredentialIds.length > 0} invalidCredential={item.invalidCredentialIds.length > 0}
actions={ actions={
<div className="flex w-16 justify-end"> <div className="flex w-16 justify-end">
@@ -143,8 +144,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}
@@ -198,31 +201,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>
); );
@@ -94,7 +94,7 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
footerText={ footerText={
!success && ( !success && (
<> <>
<Link href="/auth/login" className="font-medium text-neutral-900"> <Link href="/auth/login" className="font-medium text-gray-900">
{t("back_to_signin")} {t("back_to_signin")}
</Link> </Link>
</> </>
+7 -1
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>
+3 -6
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))}
@@ -186,11 +186,8 @@ export default function Availability({ schedule }: { schedule: number }) {
}); });
}} }}
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6"> className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1 divide-y divide-neutral-200 rounded-md border"> <div className="flex-1 divide-y divide-gray-200 rounded-md border">
<div className=" py-5 sm:p-6"> <div className=" py-5 sm:p-6">
<h3 className="mb-2 px-5 text-base font-medium leading-6 text-gray-900 sm:pl-0">
{t("change_start_end")}
</h3>
{typeof me.data?.weekStart === "string" && ( {typeof me.data?.weekStart === "string" && (
<Schedule <Schedule
control={control} control={control}
+3 -2
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";
@@ -83,7 +84,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
</div> </div>
) : ( ) : (
<div className="mb-16 overflow-hidden rounded-md border border-gray-200 bg-white"> <div className="mb-16 overflow-hidden rounded-md border border-gray-200 bg-white">
<ul className="divide-y divide-neutral-200" data-testid="schedules" ref={animationParentRef}> <ul className="divide-y divide-gray-200" data-testid="schedules" ref={animationParentRef}>
{schedules.map((schedule) => ( {schedules.map((schedule) => (
<ScheduleListItem <ScheduleListItem
displayOptions={{ displayOptions={{
+7 -8
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">
@@ -77,17 +78,15 @@ const AvailabilityView = ({ user }: { user: User }) => {
.map((slot: IBusySlot) => ( .map((slot: IBusySlot) => (
<div <div
key={dayjs(slot.start).format("HH:mm")} key={dayjs(slot.start).format("HH:mm")}
className="overflow-hidden rounded-md bg-neutral-100" className="overflow-hidden rounded-md bg-gray-100"
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")}{" "}
@@ -98,7 +97,7 @@ const AvailabilityView = ({ user }: { user: User }) => {
</div> </div>
)); ));
return ( return (
<div className="overflow-hidden rounded-md bg-neutral-100"> <div className="overflow-hidden rounded-md bg-gray-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div> <div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
</div> </div>
); );
+46 -42
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" : ""}`
@@ -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>
</> </>
)} )}
@@ -594,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 }}
@@ -636,7 +637,7 @@ export default function Success(props: SuccessProps) {
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString()) encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
: "") : "")
} }
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-gray-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-gray-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"
@@ -659,7 +660,7 @@ export default function Success(props: SuccessProps) {
eventName eventName
) + (location ? "&location=" + location : "") ) + (location ? "&location=" + location : "")
} }
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-gray-200 px-3 py-2 dark:border-gray-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"
@@ -683,7 +684,7 @@ export default function Success(props: SuccessProps) {
eventName eventName
) + (location ? "&location=" + location : "") ) + (location ? "&location=" + location : "")
} }
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-gray-200 px-3 py-2 dark:border-gray-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"
@@ -696,7 +697,7 @@ export default function Success(props: SuccessProps) {
</Link> </Link>
<Link <Link
href={"data:text/calendar," + eventLink()} 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-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
download={props.eventType.title + ".ics"}> download={props.eventType.title + ".ics"}>
<svg <svg
version="1.1" version="1.1"
@@ -845,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,
@@ -863,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: {
@@ -917,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 = (
@@ -950,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: {
@@ -1013,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 = {
@@ -1045,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,
+22 -8
View File
@@ -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;
@@ -120,7 +121,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}); });
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({
@@ -186,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,
@@ -195,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
@@ -238,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} />,
@@ -279,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,
@@ -287,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"));
@@ -304,6 +317,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
updateMutation.mutate({ updateMutation.mutate({
...input, ...input,
hosts,
locations, locations,
recurringEvent, recurringEvent,
periodStartDate: periodDates.startDate, periodStartDate: periodDates.startDate,
+21 -16
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";
@@ -251,7 +251,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const lastItem = types[types.length - 1]; const lastItem = types[types.length - 1];
return ( return (
<div className="mb-16 flex overflow-hidden rounded-md border border-gray-200 bg-white"> <div className="mb-16 flex overflow-hidden rounded-md border border-gray-200 bg-white">
<ul ref={parent} className="!static w-full divide-y divide-neutral-200" data-testid="event-types"> <ul ref={parent} className="!static w-full divide-y divide-gray-200" data-testid="event-types">
{types.map((type, index) => { {types.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`; const embedLink = `${group.profile.slug}/${type.slug}`;
const calLink = `${CAL_URL}/${embedLink}`; const calLink = `${CAL_URL}/${embedLink}`;
@@ -279,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 || "",
@@ -313,7 +312,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<Button <Button
color="secondary" color="secondary"
target="_blank" target="_blank"
size="icon" variant="icon"
href={calLink} href={calLink}
StartIcon={Icon.FiExternalLink} StartIcon={Icon.FiExternalLink}
/> />
@@ -322,7 +321,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<Tooltip content={t("copy_link")}> <Tooltip content={t("copy_link")}>
<Button <Button
color="secondary" color="secondary"
size="icon" variant="icon"
StartIcon={Icon.FiLink} StartIcon={Icon.FiLink}
onClick={() => { onClick={() => {
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
@@ -337,7 +336,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
className="radix-state-open:rounded-r-md"> className="radix-state-open:rounded-r-md">
<Button <Button
type="button" type="button"
size="icon" variant="icon"
color="secondary" color="secondary"
StartIcon={Icon.FiMoreHorizontal} StartIcon={Icon.FiMoreHorizontal}
/> />
@@ -371,11 +370,12 @@ 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);
@@ -396,7 +396,12 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<div className="min-w-9 mx-5 flex sm:hidden"> <div className="min-w-9 mx-5 flex sm:hidden">
<Dropdown> <Dropdown>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}> <DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Button type="button" size="icon" color="secondary" StartIcon={Icon.FiMoreHorizontal} /> <Button
type="button"
variant="icon"
color="secondary"
StartIcon={Icon.FiMoreHorizontal}
/>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
<DropdownMenuContent> <DropdownMenuContent>
@@ -467,7 +472,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("duplicate")} {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={() => {
@@ -512,12 +517,12 @@ 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"}>
<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={8} size="sm"
className="mt-1 inline ltr:mr-2 rtl:ml-2" className="mt-1 inline ltr:mr-2 rtl:ml-2"
/> />
</Link> </Link>
@@ -528,7 +533,7 @@ const EventTypeListHeading = ({
{profile?.name || ""} {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`}>
<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" />
@@ -538,7 +543,7 @@ const EventTypeListHeading = ({
</span> </span>
)} )}
{profile?.slug && ( {profile?.slug && (
<Link href={`${CAL_URL}/${profile.slug}`} className="block text-xs text-neutral-500"> <Link href={`${CAL_URL}/${profile.slug}`} className="block text-xs text-gray-500">
{`${CAL_URL?.replace("https://", "")}/${profile.slug}`} {`${CAL_URL?.replace("https://", "")}/${profile.slug}`}
</Link> </Link>
)} )}
@@ -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();
+1 -1
View File
@@ -29,7 +29,7 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
</div> </div>
<div className="flex-shrink-0 pt-3 sm:ml-auto sm:pt-0 sm:pl-3">{children}</div> <div className="flex-shrink-0 pt-3 sm:ml-auto sm:pt-0 sm:pl-3">{children}</div>
</section> </section>
<hr className="border-neutral-200" /> <hr className="border-gray-200" />
</> </>
); );
}; };
@@ -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;
@@ -121,7 +123,7 @@ const AppearanceView = () => {
/> />
</div> </div>
<hr className="border-1 my-8 border-neutral-200" /> <hr className="my-8 border border-gray-200" />
<div className="mb-6 flex items-center text-sm"> <div className="mb-6 flex items-center text-sm">
<div> <div>
<p className="font-semibold">{t("custom_brand_colors")}</p> <p className="font-semibold">{t("custom_brand_colors")}</p>
@@ -167,7 +169,7 @@ const AppearanceView = () => {
onClick={() => window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}> onClick={() => window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}>
Preview Preview
</Button> */} </Button> */}
<hr className="border-1 my-8 border-neutral-200" /> <hr className="my-8 border border-gray-200" />
<Controller <Controller
name="hideBranding" name="hideBranding"
control={formMethods.control} control={formMethods.control}
@@ -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,18 +13,18 @@ import {
Dropdown, Dropdown,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownItem,
DropdownMenuTrigger, DropdownMenuTrigger,
Icon, Icon,
List, List,
ListItem,
ListItemText,
ListItemTitle,
Meta, Meta,
showToast, showToast,
SkeletonContainer, SkeletonContainer,
SkeletonText, SkeletonText,
} from "@calcom/ui"; } from "@calcom/ui";
import AppListCard from "@components/AppListCard";
import { ssrInit } from "@server/lib/ssr"; import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
@@ -75,41 +75,37 @@ const ConferencingLayout = () => {
apps.items apps.items
.map((app) => ({ ...app, title: app.title || app.name })) .map((app) => ({ ...app, title: app.title || app.name }))
.map((app) => ( .map((app) => (
<ListItem className="flex-col border-0" key={app.title}> <AppListCard
<div className="flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse"> description={app.description}
{ title={app.title}
// eslint-disable-next-line @next/next/no-img-element logo={app.logo}
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} /> key={app.title}
} isDefault={app.isGlobal}
<div className="flex-grow truncate pl-2"> actions={
<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>
</ListItemTitle>
<ListItemText component="p">{app.description}</ListItemText>
</div>
<div> <div>
<Dropdown> <Dropdown>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button StartIcon={Icon.FiMoreHorizontal} size="icon" color="secondary" /> <Button StartIcon={Icon.FiMoreHorizontal} variant="icon" color="secondary" />
</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>
</div> </div>
</div> }
</ListItem> />
))} ))}
</List> </List>
@@ -223,17 +223,13 @@ const ProfileView = () => {
} }
/> />
<hr className="my-6 border-neutral-200" /> <hr className="my-6 border-gray-200" />
<Label>{t("danger_zone")}</Label> <Label>{t("danger_zone")}</Label>
{/* 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 {
+4 -6
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) {
@@ -37,12 +36,12 @@ function TeamPage({ team }: TeamPageProps) {
}, [telemetry, router.asPath]); }, [telemetry, router.asPath]);
const EventTypes = () => ( const EventTypes = () => (
<ul className="rounded-md border border-neutral-200 dark:border-neutral-700"> <ul className="rounded-md border border-gray-200 dark:border-gray-700">
{team.eventTypes.map((type, index) => ( {team.eventTypes.map((type, index) => (
<li <li
key={index} key={index}
className={classNames( className={classNames(
"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-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600",
!isEmbed && "bg-white" !isEmbed && "bg-white"
)}> )}>
<Link <Link
@@ -57,10 +56,9 @@ function TeamPage({ team }: TeamPageProps) {
</div> </div>
<div className="mt-1 self-center"> <div className="mt-1 self-center">
<AvatarGroup <AvatarGroup
border="border-2 border-white dark:border-darkgray-100"
truncateAfter={4} truncateAfter={4}
className="flex flex-shrink-0" className="flex flex-shrink-0"
size={10} size="sm"
items={type.users.map((user) => ({ items={type.users.map((user) => ({
alt: user.name || "", alt: user.name || "",
title: user.name || "", title: user.name || "",
+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,
})), })),
}); });
+3 -2
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";
@@ -15,7 +16,7 @@ function Teams() {
subtitle={t("create_manage_teams_collaborative")} subtitle={t("create_manage_teams_collaborative")}
CTA={ CTA={
<Button <Button
size="fab" variant="fab"
StartIcon={Icon.FiPlus} StartIcon={Icon.FiPlus}
type="button" type="button"
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}> href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
@@ -426,7 +426,6 @@
"password_hint_num": "يحتوي على رقم واحد على الأقل", "password_hint_num": "يحتوي على رقم واحد على الأقل",
"invalid_password_hint": "يجب أن تتألف كلمة المرور على الأقل من 7 أحرف، ورقم واحد على الأقل، ومزيج من الحروف الكبيرة والصغيرة", "invalid_password_hint": "يجب أن تتألف كلمة المرور على الأقل من 7 أحرف، ورقم واحد على الأقل، ومزيج من الحروف الكبيرة والصغيرة",
"incorrect_password": "كلمة المرور غير صحيحة.", "incorrect_password": "كلمة المرور غير صحيحة.",
"1_on_1": "اجتماع 1 مع 1",
"24_h": "24 ساعة", "24_h": "24 ساعة",
"use_setting": "استخدام الإعداد", "use_setting": "استخدام الإعداد",
"am_pm": "صباحًا/مساءً", "am_pm": "صباحًا/مساءً",
@@ -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.",
@@ -426,7 +426,6 @@
"password_hint_num": "Obsahuje alespoň 1 číslo", "password_hint_num": "Obsahuje alespoň 1 číslo",
"invalid_password_hint": "Heslo musí být minimálně 7 znaků dlouhé, obsahovat alespoň jedno číslo a obsahovat kombinaci velkých a malých písmen", "invalid_password_hint": "Heslo musí být minimálně 7 znaků dlouhé, obsahovat alespoň jedno číslo a obsahovat kombinaci velkých a malých písmen",
"incorrect_password": "Heslo není správné.", "incorrect_password": "Heslo není správné.",
"1_on_1": "Jeden na jednoho",
"24_h": "24h", "24_h": "24h",
"use_setting": "Použít nastavení", "use_setting": "Použít nastavení",
"am_pm": "am/pm", "am_pm": "am/pm",
@@ -9,7 +9,7 @@
"have_any_questions": "Haben Sie Fragen? Wir sind hier um Ihnen zu helfen.", "have_any_questions": "Haben Sie Fragen? Wir sind hier um Ihnen zu helfen.",
"reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts", "reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts",
"event_declined_subject": "Abgelehnt: {{eventType}} mit {{name}} am {{date}}", "event_declined_subject": "Abgelehnt: {{eventType}} mit {{name}} am {{date}}",
"event_cancelled_subject": "Abgesagt: {{eventType}} mit {{name}} am {{date}}", "event_cancelled_subject": "Storniert: {{title}} um {{date}}",
"event_request_declined": "Ihre Event-Anfrage wurde abgelehnt", "event_request_declined": "Ihre Event-Anfrage wurde abgelehnt",
"event_request_declined_recurring": "Ihre wiederkehrende Terminanfrage wurde abgelehnt", "event_request_declined_recurring": "Ihre wiederkehrende Terminanfrage wurde abgelehnt",
"event_request_cancelled": "Ihr geplanter Termin wurde abgesagt", "event_request_cancelled": "Ihr geplanter Termin wurde abgesagt",
@@ -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.",
@@ -425,7 +425,6 @@
"password_hint_num": "Enthält mindestens 1 Zahl", "password_hint_num": "Enthält mindestens 1 Zahl",
"invalid_password_hint": "Das Passwort muss mindestens 7 Zeichen lang sein und mindestens eine Nummer beinhalten, sowie eine Mischung aus Groß- und Kleinbuchstaben sein", "invalid_password_hint": "Das Passwort muss mindestens 7 Zeichen lang sein und mindestens eine Nummer beinhalten, sowie eine Mischung aus Groß- und Kleinbuchstaben sein",
"incorrect_password": "Passwort ist falsch", "incorrect_password": "Passwort ist falsch",
"1_on_1": "1-on-1",
"24_h": "24 Std", "24_h": "24 Std",
"use_setting": "Benutze Einstellung", "use_setting": "Benutze Einstellung",
"am_pm": "am/pm", "am_pm": "am/pm",
+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": "Απενεργοποίηση επισκεπτών"
} }
+25 -8
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",
@@ -23,6 +23,7 @@
"rejection_reason_description": "Are you sure you want to reject the booking? We'll let the person who tried to book know. You can provide a reason below.", "rejection_reason_description": "Are you sure you want to reject the booking? We'll let the person who tried to book know. You can provide a reason below.",
"rejection_confirmation": "Reject the booking", "rejection_confirmation": "Reject the booking",
"manage_this_event": "Manage this event", "manage_this_event": "Manage this event",
"invite_team_member": "Invite team member",
"your_event_has_been_scheduled": "Your event has been scheduled", "your_event_has_been_scheduled": "Your event has been scheduled",
"your_event_has_been_scheduled_recurring": "Your recurring event has been scheduled", "your_event_has_been_scheduled_recurring": "Your recurring event has been scheduled",
"accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.", "accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
@@ -32,7 +33,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 +65,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: {{title}} 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.",
@@ -429,7 +430,6 @@
"password_hint_num": "Contain at least 1 number", "password_hint_num": "Contain at least 1 number",
"invalid_password_hint": "The password must be a minimum of 7 characters long containing at least one number and have a mixture of uppercase and lowercase letters", "invalid_password_hint": "The password must be a minimum of 7 characters long containing at least one number and have a mixture of uppercase and lowercase letters",
"incorrect_password": "Password is incorrect.", "incorrect_password": "Password is incorrect.",
"1_on_1": "1-on-1",
"24_h": "24h", "24_h": "24h",
"use_setting": "Use setting", "use_setting": "Use setting",
"am_pm": "am/pm", "am_pm": "am/pm",
@@ -660,6 +660,7 @@
"edit_availability": "Edit availability", "edit_availability": "Edit availability",
"configure_availability": "Configure times when you are available for bookings.", "configure_availability": "Configure times when you are available for bookings.",
"copy_times_to": "Copy times to", "copy_times_to": "Copy times to",
"copy_times_to_tooltip": "Copy times to …",
"change_weekly_schedule": "Change your weekly schedule", "change_weekly_schedule": "Change your weekly schedule",
"logo": "Logo", "logo": "Logo",
"error": "Error", "error": "Error",
@@ -789,6 +790,7 @@
"number_apps_one": "{{count}} App", "number_apps_one": "{{count}} App",
"number_apps_other": "{{count}} Apps", "number_apps_other": "{{count}} Apps",
"trending_apps": "Trending Apps", "trending_apps": "Trending Apps",
"most_popular":"Most Popular",
"explore_apps": "{{category}} apps", "explore_apps": "{{category}} apps",
"installed_apps": "Installed Apps", "installed_apps": "Installed Apps",
"free_to_use_apps": "Free", "free_to_use_apps": "Free",
@@ -799,12 +801,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.",
@@ -1217,6 +1221,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",
@@ -1460,6 +1465,7 @@
"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",
"your_bookings_filter_label":"Your Bookings",
"meeting_url_variable": "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",
@@ -1492,10 +1498,21 @@
"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", "requires_google_calendar": "This app requires a Google Calendar connection",
"connected_google_calendar": "You have connected a Google Calendar account.", "connected_google_calendar": "You have connected a Google Calendar account.",
"using_meet_requires_calendar": "Using Google Meet requires a connected Google Calendar", "using_meet_requires_calendar": "Using Google Meet requires a connected Google Calendar",
"continue_to_install_google_calendar": "Continue to install Google Calendar", "continue_to_install_google_calendar": "Continue to install Google Calendar",
"install_google_meet": "Install Google Meet", "install_google_meet": "Install Google Meet",
"install_google_calendar": "Install Google Calendar" "install_google_calendar": "Install Google Calendar",
"sender_name": "Sender name",
"no_recordings_found": "No recordings found"
} }
@@ -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.",
@@ -426,7 +426,6 @@
"password_hint_num": "Debe contener al menos 1 número", "password_hint_num": "Debe contener al menos 1 número",
"invalid_password_hint": "La contraseña debe tener un mínimo de 7 caracteres que contengan al menos un número y una mezcla de letras mayúsculas y minúsculas", "invalid_password_hint": "La contraseña debe tener un mínimo de 7 caracteres que contengan al menos un número y una mezcla de letras mayúsculas y minúsculas",
"incorrect_password": "La Contraseña es Incorrecta.", "incorrect_password": "La Contraseña es Incorrecta.",
"1_on_1": "1 a 1",
"24_h": "24hs", "24_h": "24hs",
"use_setting": "Usar Ajuste", "use_setting": "Usar Ajuste",
"am_pm": "am/pm", "am_pm": "am/pm",
Loaded 100 of 510 files, more files were not shown because too many files have changed in this diff. Show more