97 Commits
V1.5 ... V1.5.3

Author SHA1 Message Date
zomars
de0883b14b v1.5.3 2022-05-02 16:07:45 -06:00
zomars
acc6db901c Migration seeder fixes 2022-05-02 16:02:45 -06:00
Joe Au-Yeung
7f463830bd Add Meta Mask to app store (#2650)
* Adds available apps

* Adds App Model

* WIP

* Create meta mask app folder

* Add description and images

* Remove credential from installed apps page

* Updates seeder script

* Seeder fixes

* lowercase categories

* Upgrades prisma

* WIP

* WIP

* Hopefully fixes circular deps

* Type fixes

* Fixes seeder

* Adds migration to connect Credentials to Apps

* Updates app store callbacks

* Updates google credentials

* Uses dirName from DB

* Type fixes

* Update reschedule.ts

* Seeder fixes

* Fixes categories listing

* Update index.ts

* Update schema.prisma

* Updates dependencies

* Renames giphy app

* Uses dynamic imports for app metadata

* Fixes credentials error

* Uses dynamic import for api handlers

* Dynamic import fixes

* Allows for simple folder names in app store

* Remove video adaptor

* Squashes app migrations

* seeder fixes

* Renames to metamask

* Updates metamask metadata

* Fixes dyamic imports

* Remove comments

* Create migration.sql

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

* Adds App Model

* WIP

* Updates seeder script

* Seeder fixes

* lowercase categories

* Upgrades prisma

* WIP

* WIP

* Hopefully fixes circular deps

* Type fixes

* Fixes seeder

* Adds migration to connect Credentials to Apps

* Updates app store callbacks

* Updates google credentials

* Uses dirName from DB

* Type fixes

* Update reschedule.ts

* Seeder fixes

* Fixes categories listing

* Update index.ts

* Update schema.prisma

* Updates dependencies

* Renames giphy app

* Uses dynamic imports for app metadata

* Fixes credentials error

* Uses dynamic import for api handlers

* Dynamic import fixes

* Allows for simple folder names in app store

* Squashes app migrations

* seeder fixes

* Fixes dyamic imports

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

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

* Change calendar error message

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

* disposable model updated

* added disposable slug availability page

* added disposable book page

* added disposable slug hook

* added disposable link booking flow

* updated schema

* checktype fix

* added checkfix and schema generated

* create link API added

* added one time link view on event type list

* adjusted schema

* fixed disposable visual indicator

* expired check and visual indicator added

* updated slug for disposable event type

* revised schema

* WIP

* revert desc

* revert --WIP

* rework based on change of plans

* further adjustments

* added eventtype option for hashed link

* added refresh and delete on update

* fixed update call conditions

* cleanup

* code improvement

* clean up

* Potential fix for 404

* backward compat for booking page

* fixes regular booking for user and team

* typefix

* updated path for Booking import

* checkfix

* e2e wip

* link err fix

* workaround for banner issue in event type update-test

* added regenerate hash check

* fixed test according to new testID

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

* Tidy up console logs

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

* Accept license for tests

* Accept license on preview

* Remove debugging code

* Add License consent flag

* Test fixes

* Update playwright.config.ts

* Update webhookResponse-chromium.txt

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

* fix: remove unneded import

* fix: adds yarn dev commands for api and swagger

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

* Fixes email notes

* Fixing reschedule email

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

* Update form values when query changes

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

* Fixes Flicker + Null return

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

* Remove Href

* Fix Height Jumping around

* Subtle Colors

* feedback by ciaran

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

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

* fix: rename verificationRequest -> verificationToken in the codebase

* feat: add default cookies for next-auth

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

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

* fix verificationTokenToken typo

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

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

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

* update NEXTAUTH_DOMAIN to NEXTAUTH_COOKIE_DOMAIN

* Updates website submodule

* Removes deprecated env vars

* Consolidates auth logic in one place

* Updates website module

* Signup fixes

* Build fixes

* Updates example

* Updates example

* Fixes

* Fix Email Verification

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

* Removes console log

* Fixes link in email template

* Removed irrelevant coment

* Testing with a 32 bit secret

* Fixes for cookien in E2E

* E2E fixes

* Fixes Stripe tests locally

* Temp fix for E2E

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

* Add Jitsi add button

* Find app credentials based on variant

* Make huddle installable

* Remove default installed message from installed apps page

* Display Jitsi and Huddle as locations if installed

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

* Changes to app page refrence #2556

* Fix type errors

* Revert code

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

* Fix email subject for reschedule

* Fix email subject for reschedule

* Fix api add wipemycal return

* Now we ask on a endpoint if app its installed

* Fix types

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

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

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

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

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

This reverts commit 2938d510c874e9298b82465baeae5af9181c01d8.

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

This reverts commit 848e1a153538475acb42daa6ddbd74dd474611f5.

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

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

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

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

* fix 30 day -> 30 days in expiresAt

* feat: api keys frontend in security page

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

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

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

* feat: hashed api keys

* fix: minor refactor and cleanup in apiKeys generator

* add api key success modal

* sync apps/api

* feat: We have API Keys in Security =)

* remove swagger env from pr

* apps api sync

* remove comments in password section

* feat: migration for api keys schema

* sync api w main

* delete apps/api

* add back apps/api

* make min date and disabled optional props in datepicker

* feat fix type check errors

* fix : types

* fix: rmeove renaming of verificationrequest token indexes in migration

* fix: remove extra div

* Fixes for feedback in PR

* fix button />

* fix: rename weird naming of translation for you_will_only_view_it_once

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

* fix sync apps/api with main not old commit

* fix empty className

* fix: remove unused imports

* fix remove commented jsx fragment close

* fix rename editing

* improve translations

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

* fix: use api keys everywhere

* fix: cleanup code in api keys

* fix: use watch and controller for neverexpires/datepicker

* Fixes: improve api key never expires

* add back change password h2 title section in security page

* fix update env API_KEY_ prefix default to cal_

* fix: improve eidt api keys modal

* fix: update edit mutation in viewer.apiKeys

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

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

* fix: item: any to pass build

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

* OAuth done and credentials stored

* Added "other" integrations

* Switching to hubspot api client

* Event creation for all attendees

* Update and delete done

* Doc update

* Fixing types

* App label is not mandatory

* Fixing bad merge: App label deleted

* Fixing bad automerge

* Removing  c.log

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

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

Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-04-16 01:58:07 +00:00
Agusti Fernandez
7e392da78a remove vercel favicon and logo, remove empty space on top due to Head (#2522)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
2022-04-16 03:35:49 +02:00
Agusti Fernandez
f8f3456b92 Adds iframe to developer.cal.com (swagger-ui) into docs/public-api.mdx (#2518)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
2022-04-15 23:56:32 +00:00
Agusti Fernandez
3b637eefaa adds redirect for /api to developer.cal.com (#2517)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
2022-04-15 14:19:52 -06:00
Agusti Fernandez
46e1d28881 improve styling on mobile for swagger docs (#2516)
Co-authored-by: Agusti Fernandez Pardo <git@agusti.me>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-04-15 12:52:51 -06:00
Agusti Fernandez
f23cc8b99f Adds env example and changes route (#2515) 2022-04-15 19:11:35 +01:00
Hariom Balhara
6843347dd7 Embed: Bump Versions and update Readmes and descriptions (#2507) 2022-04-15 18:55:36 +01:00
313 changed files with 9203 additions and 4422 deletions

View File

@@ -8,11 +8,13 @@
# - APP STORE
# - DAILY.CO VIDEO
# - GOOGLE CALENDAR/MEET/LOGIN
# - HUBSPOT
# - OFFICE 365
# - SLACK
# - STRIPE
# - TANDEM
# - ZOOM
# - GIPHY
# - LICENSE *************************************************************************************************
# Set this value to 'agree' to accept our license:
@@ -47,10 +49,13 @@ PGSSLMODE=
# - NEXTAUTH
# @see: https://github.com/calendso/calendso/issues/263
# Required for Vercel hosting - set NEXTAUTH_URL to equal your BASE_URL
# @see: https://next-auth.js.org/configuration/options#nextauth_url
# Required for Vercel hosting - set NEXTAUTH_URL to equal your NEXT_PUBLIC_WEBAPP_URL
# NEXTAUTH_URL='http://localhost:3000'
NEXTAUTH_URL=
JWT_SECRET='secret'
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
# You can use: `openssl rand -base64 32` to generate one
NEXTAUTH_SECRET=
# Used for cross-domain cookie authentication
NEXTAUTH_COOKIE_DOMAIN=.example.com
@@ -83,6 +88,9 @@ NEXT_PUBLIC_STRIPE_PRO_PLAN_PRODUCT=
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE=
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE=
# Use for internal Public API Keys and optional
API_KEY_PREFIX=cal_
# ***********************************************************************************************************
# - E-MAIL SETTINGS *****************************************************************************************
@@ -109,6 +117,7 @@ EMAIL_SERVER_PASSWORD='<office365_password>'
# **********************************************************************************************************
# - APP STORE **********************************************************************************************
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
# - DAILY.CO VIDEO
DAILY_API_KEY=
DAILY_SCALE_PLAN=''
@@ -124,6 +133,12 @@ GOOGLE_API_CREDENTIALS='{}'
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
GOOGLE_LOGIN_ENABLED=false
# - HUBSPOT
# Used for the HubSpot integration
# @see https://github.com/calcom/cal.com/#obtaining-hubspot-client-id-and-secret
HUBSPOT_CLIENT_ID=""
HUBSPOT_CLIENT_SECRET=""
# - OFFICE 365
# Used for the Office 365 / Outlook.com Calendar / MS Teams integration
# @see https://github.com/calcom/cal.com/#Obtaining-Microsoft-Graph-Client-ID-and-Secret
@@ -145,7 +160,7 @@ PAYMENT_FEE_FIXED=10 # Take 10 additional cents commission
PAYMENT_FEE_PERCENTAGE=0.005 # Take 0.5% commission
# - TANDEM
# Used for the Tandem integration -- contact support@tandem.chat to for API access.
# Used for the Tandem integration -- contact support@tandem.chat for API access.
TANDEM_CLIENT_ID=""
TANDEM_CLIENT_SECRET=""
TANDEM_BASE_URL="https://tandem.chat"
@@ -155,4 +170,9 @@ TANDEM_BASE_URL="https://tandem.chat"
# @see https://github.com/calcom/cal.com/#obtaining-zoom-client-id-and-secret
ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET=
# - GIPHY
# Used for the Giphy integration
# @see https://support.giphy.com/hc/en-us/articles/360020283431-Request-A-GIPHY-API-Key
GIPHY_API_KEY=
# *********************************************************************************************************

View File

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

View File

@@ -19,7 +19,7 @@ jobs:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
JWT_SECRET: secret
NEXTAUTH_SECRET: secret
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: true
# CRON_API_KEY: xxx

4
.gitmodules vendored
View File

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

128
CODE_OF_CONDUCT.md Normal file
View File

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

View File

@@ -102,18 +102,13 @@ Here is what you need to be able to run Cal.
cd cal.com
```
1. Copy `apps/web/.env.example` to `apps/web/.env`
```sh
cp apps/web/.env.example apps/web/.env
cp packages/prisma/.env.example packages/prisma/.env
```
1. Install packages with yarn
```sh
yarn
```
1. Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the .env file.
#### Quick start with `yarn dx`
@@ -126,10 +121,10 @@ yarn dx
#### Development tip
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `apps/web/.env` to get logging information for all the queries and mutations driven by **trpc**.
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **trpc**.
```sh
echo 'NEXT_PUBLIC_DEBUG=1' >> apps/web/.env
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
```
#### Manual setup
@@ -196,10 +191,8 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> apps/web/.env
### E2E-Testing
```sh
# In first terminal. Must run on port 3000.
yarn dx
# In second terminal
yarn workspace @calcom/web test-e2e
# In a terminal. Just run:
yarn test-e2e
# To open last HTML report run:
yarn workspace @calcom/web playwright-report
@@ -213,7 +206,13 @@ yarn workspace @calcom/web playwright-report
git pull
```
2. Apply database migrations by running <b>one of</b> the following commands:
1. Check if dependencies got added/updated/removed
```sh
yarn
```
1. Apply database migrations by running <b>one of</b> the following commands:
In a development environment, run:
@@ -229,16 +228,13 @@ yarn workspace @calcom/web playwright-report
yarn workspace @calcom/prisma db-deploy
```
3. Check the `.env.example` and compare it to your current `.env` file. In case there are any fields not present
in your current `.env`, add them there.
1. Check for `.env` variables changes
For the current version, especially check if the variable `BASE_URL` is present and properly set in your environment, for example:
```sh
yarn predev
```
```
BASE_URL='https://yourdomain.com'
```
4. Start the server. In a development environment, just do:
1. Start the server. In a development environment, just do:
```sh
yarn dev
@@ -251,7 +247,7 @@ yarn workspace @calcom/web playwright-report
yarn start
```
5. Enjoy the new version.
1. Enjoy the new version.
<!-- DEPLOYMENT -->
## Deployment
@@ -349,6 +345,7 @@ oauth_config:
bot:
- chat:write
- commands
- chat:write.public
settings:
interactivity:
is_enabled: true
@@ -391,6 +388,19 @@ Next make sure you have your app running `yarn dx`. Then in the slack chat type
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
5. If you have the [Daily Scale Plan](https://www.daily.co/pricing) set the `DAILY_SCALE_PLAN` variable to `true` in order to use features like video recording.
### Obtaining HubSpot Client ID and Secret
1. Open [HubSpot Developer](https://developer.hubspot.com/) and sign into your account, or create a new one.
2. From within the home of the Developer account page, go to "Manage apps".
3. Click "Create app" button top right.
4. Fill in any information you want in the "App info" tab
5. Go to tab "Auth"
6. Now copy the Client ID and Client Secret to your .env file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspot othercalendar/callback` replacing Cal.com URL with the URI at which your application runs.
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scope called `crm.objects.contacts`
9. Click the "Save" button at the bottom footer.
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
<!-- LICENSE -->
## License

View File

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

1
apps/admin Submodule

Submodule apps/admin added at cf71a8b47e

View File

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

View File

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

View File

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

View File

@@ -8,11 +8,9 @@ The Embed allows your website visitors to book a meeting with you directly from
## Install on any website
TODO: Mention possibility of installation through tag managers as well
- _Step-1._ Install the Vanilla JS Snippet
```javascript
```html
<script>
(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
@@ -40,14 +38,10 @@ TODO: Mention possibility of installation through tag managers as well
}
p(cal, ar);
};
})(window, "https://cal.com/embed.js", "init");
```
- _Step-2_. Initialize it
```javascript
Cal("init)
```
})(window, "https://cal.com/embed.js", "init");
Cal("init")
</script>
```
## Install with a Framework
@@ -65,7 +59,7 @@ You can use Vanilla JS Snippet to install
## Popular ways in which you can embed on your website
Assuming that you have followed the steps of installing and initializing the snippet, you can add show the embed in following ways:
Assuming that you have followed the steps of installing and initializing the snippet, you can show the embed in following ways:
### Inline
@@ -74,18 +68,20 @@ Show the embed inline inside a container element. It would take the width and he
<details>
<summary>_Vanilla JS_</summary>
```javascript
Cal("inline", {
elementOrSelector: "Your Embed Container Selector Path", // You can also provide an element directly
calLink: "jane", // The link that you want to embed. It would open https://cal.com/jane in embed
config: {
name: "John Doe", // Prefill Name
email: "johndoe@gmail.com", // Prefill Email
notes: "Test Meeting", // Prefill Notes
guests: ["janedoe@gmail.com", "test@gmail.com"], // Prefill Guests
theme: "dark", // "dark" or "light" theme
},
});
```html
<script>
Cal("inline", {
elementOrSelector: "Your Embed Container Selector Path", // You can also provide an element directly
calLink: "jane", // The link that you want to embed. It would open https://cal.com/jane in embed
config: {
name: "John Doe", // Prefill Name
email: "johndoe@gmail.com", // Prefill Email
notes: "Test Meeting", // Prefill Notes
guests: ["janedoe@gmail.com", "test@gmail.com"], // Prefill Guests
theme: "dark", // "dark" or "light" theme
},
});
</script>
```
</details>
@@ -139,7 +135,6 @@ To show the embed as a popup on clicking an element, simply add `data-cal-link`
````
</details>
### Full Screen
## Supported Instructions
@@ -149,8 +144,10 @@ Consider an instruction as a function with that name and that would be called wi
Appends embed inline as the child of the element.
```javascript
```html
<script>
Cal("inline", { elementOrSelector, calLink });
</script>
````
- `elementOrSelector` - Give it either a valid CSS selector or an HTMLElement instance directly
@@ -161,8 +158,10 @@ Cal("inline", { elementOrSelector, calLink });
Configure UI for embed. Make it look part of your webpage.
```javascript
```html
<script>
Cal("inline", { styles });
</script>
```
- `styles` - It supports styling for `body` and `eventTypeListItem`. Right now we support just background on these two.
@@ -173,15 +172,18 @@ Usage:
If you want to open cal link on some action. Make it pop open instantly by preloading it.
```javascript
```html
<script>
Cal("preload", { calLink });
</script>
```
- `calLink` - Cal Link that you want to embed e.g. john. Just give the username. No need to give the full URL [https://cal.com/john]()
## Actions
You can listen to an action that occurs in embedded cal link as follows. You can think of them as DOM events. We are avoiding the term events to not confuse it with Cal Events.
```javascript
You can listen to an action that occurs in embedded cal link as follows. You can think of them as DOM events. We are avoiding the term "events" to not confuse it with Cal Events.
```html
<script>
Cal("on", {
action: "ANY_ACTION_NAME",
callback: (e)=>{
@@ -191,11 +193,12 @@ Cal("on", {
const {data, type, namespace} = e.detail;
}
})
</script>
```
Following are the list of supported actions.
-
| action | description | properties |
| Action | Description | Properties |
|----------------------|------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| eventTypeSelected | When user chooses an event-type from the listing. | eventType:object // Event Type that has been selected" |
| bookingSuccessful | When the booking is successfully done. It might not be confirmed. | confirmed: boolean; //Whether confirmation from organizer is pending or not <br/><br/>eventType: "Object for Event Type that has been booked"; <br/><br/>date: string; // Date of Event <br/><br/>duration: number; //Duration of booked Event <br/><br/>organizer: object //Organizer details like name, timezone, email |

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
import Bleed from 'nextra-theme-docs/bleed'
import Head from "next/head";
<Bleed full>
<Head><title>Public API | Cal.com</title></Head>
<iframe src="https://developer.cal.com"
width="100%"
height="900px"
title="Public API | Cal.com"
></iframe>
</Bleed>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,45 +1,21 @@
import Head from "next/head";
import SwaggerUI from "swagger-ui-react";
import dynamic from "next/dynamic";
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
const SwaggerUI: any = dynamic(() => import("swagger-ui-react"), { ssr: false });
const requestSnippets = {
generators: {
curl_bash: {
title: "cURL (bash)",
syntax: "bash",
},
curl_powershell: {
title: "cURL (PowerShell)",
syntax: "powershell",
},
curl_cmd: {
title: "cURL (CMD)",
syntax: "bash",
},
node: {
title: "Node",
syntax: "node",
},
},
defaultExpanded: true,
languages: ["curl_bash"],
// e.g. only show curl bash = ["curl_bash"]
};
export default function APIDocs() {
return (
<div>
<Head>
<title>Cal.com - Docs - SwaggerUI</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<SwaggerUI
requestSnippets={requestSnippets}
requestSnippetsEnabled={true}
docExpansion="none"
operationsSorter="method"
filter={true}
url={process.env.NEXT_PUBLIC_SWAGGER_DOCS_URL || "https://api.cal.com/api/docs"}
/>
</div>
<SwaggerUI
url={process.env.NEXT_PUBLIC_SWAGGER_DOCS_URL || "https://api.cal.com/docs"}
supportedSubmitMethods={["get", "post", "delete", "patch"]}
requestSnippetsEnabled={true}
requestSnippets={requestSnippets}
plugins={[SnippedGenerator]}
tryItOutEnabled={true}
syntaxHighlight={true}
docExpansion="none"
filter={true}
/>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,4 +0,0 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,11 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"baseUrl": ".",
"paths": {
"@lib/*": ["lib/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]

View File

@@ -8,14 +8,13 @@ import {
} from "@heroicons/react/outline";
import { ChevronLeftIcon } from "@heroicons/react/solid";
import Link from "next/link";
import React from "react";
import React, { useEffect, useState } from "react";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { App as AppType } from "@calcom/types/App";
import { Button } from "@calcom/ui";
//import NavTabs from "@components/NavTabs";
import Shell from "@components/Shell";
import Badge from "@components/ui/Badge";
@@ -60,7 +59,29 @@ export default function App({
currency: "USD",
useGrouping: false,
}).format(price);
const [installedApp, setInstalledApp] = useState(false);
useEffect(() => {
async function getInstalledApp(appCredentialType: string) {
const queryParam = new URLSearchParams();
queryParam.set("app-credential-type", appCredentialType);
try {
const result = await fetch(`/api/app-store/installed?${queryParam.toString()}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (result.status === 200) {
setInstalledApp(true);
}
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
}
}
}
getInstalledApp(type);
}, []);
return (
<>
<Shell large>
@@ -83,7 +104,7 @@ export default function App({
</div>
<div className="mt-4 sm:mt-0 sm:text-right">
{isGlobal ? (
{isGlobal || installedApp ? (
<Button color="secondary" disabled title="This app is globally installed">
{t("installed")}
</Button>

View File

@@ -24,7 +24,7 @@ export default function AppsShell({ children }: { children: React.ReactNode }) {
<div className="mb-12 block lg:hidden">
{status === "authenticated" && <NavTabs tabs={tabs} linkProps={{ shallow: true }} />}
</div>
<main>{children}</main>
<main className="pb-6">{children}</main>
</>
);
}

View File

@@ -1,49 +1,52 @@
import { AdminRequired } from "components/ui/AdminRequired";
import Link, { LinkProps } from "next/link";
import { useRouter } from "next/router";
import React, { ElementType, FC } from "react";
import React, { ElementType, FC, Fragment } from "react";
import classNames from "@lib/classNames";
interface Props {
export interface NavTabProps {
tabs: {
name: string;
href: string;
icon?: ElementType;
adminRequired?: boolean;
}[];
linkProps?: Omit<LinkProps, "href">;
}
const NavTabs: FC<Props> = ({ tabs, linkProps }) => {
const NavTabs: FC<NavTabProps> = ({ tabs, linkProps }) => {
const router = useRouter();
return (
<>
<nav
className="-mb-px flex space-x-2 space-x-5 rtl:space-x-reverse sm:rtl:space-x-reverse"
aria-label="Tabs">
<nav className="-mb-px flex space-x-5 rtl:space-x-reverse sm:rtl:space-x-reverse" aria-label="Tabs">
{tabs.map((tab) => {
const isCurrent = router.asPath === tab.href;
const Component = tab.adminRequired ? AdminRequired : Fragment;
return (
<Link key={tab.name} href={tab.href} {...linkProps}>
<a
className={classNames(
isCurrent
? "border-neutral-900 text-neutral-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium"
)}
aria-current={isCurrent ? "page" : undefined}>
{tab.icon && (
<tab.icon
className={classNames(
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
"-ml-0.5 hidden h-5 w-5 ltr:mr-2 rtl:ml-2 sm:inline-block"
)}
aria-hidden="true"
/>
)}
<span>{tab.name}</span>
</a>
</Link>
<Component key={tab.name}>
<Link href={tab.href} {...linkProps}>
<a
className={classNames(
isCurrent
? "border-neutral-900 text-neutral-900"
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium"
)}
aria-current={isCurrent ? "page" : undefined}>
{tab.icon && (
<tab.icon
className={classNames(
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
"-ml-0.5 hidden h-5 w-5 ltr:mr-2 rtl:ml-2 sm:inline-block"
)}
aria-hidden="true"
/>
)}
<span>{tab.name}</span>
</a>
</Link>
</Component>
);
})}
</nav>

View File

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

View File

@@ -11,6 +11,7 @@ import {
MoonIcon,
ViewGridIcon,
} from "@heroicons/react/solid";
import { UserPlan } from "@prisma/client";
import { SessionContextValue, signOut, useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -19,7 +20,6 @@ import { Toaster } from "react-hot-toast";
import { useIsEmbed } from "@calcom/embed-core";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { UserPlan } from "@calcom/prisma/client";
import Button from "@calcom/ui/Button";
import Dropdown, {
DropdownMenuContent,
@@ -40,6 +40,7 @@ import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";
import ImpersonatingBanner from "@components/ui/ImpersonatingBanner";
import pkg from "../package.json";
import { useViewerI18n } from "./I18nLanguageHandler";
@@ -69,7 +70,7 @@ function useRedirectToLoginIfUnauthenticated(isPublic = false) {
router.replace({
pathname: "/auth/login",
query: {
callbackUrl: `${WEBAPP_URL}/${location.pathname}${location.search}`,
callbackUrl: `${WEBAPP_URL}${location.pathname}${location.search}`,
},
});
}
@@ -125,9 +126,10 @@ const Layout = ({
status,
plan,
...props
}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan }) => {
}: LayoutProps & { status: SessionContextValue["status"]; plan?: UserPlan; isLoading: boolean }) => {
const isEmbed = useIsEmbed();
const router = useRouter();
const { t } = useLocale();
const navigation = [
{
@@ -311,6 +313,7 @@ const Layout = ({
props.flexChildrenContainer && "flex flex-1 flex-col",
!props.large && "py-8"
)}>
<ImpersonatingBanner />
{!!props.backPath && (
<div className="mx-3 mb-8 sm:mx-8">
<Button
@@ -329,10 +332,21 @@ const Layout = ({
)}>
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
<div className="mb-8 w-full">
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
{props.heading}
</h1>
<p className="min-h-10 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
{props.isLoading ? (
<>
<div className="mb-1 h-6 w-24 animate-pulse rounded-md bg-gray-200"></div>
<div className="mb-1 h-6 w-32 animate-pulse rounded-md bg-gray-200"></div>
</>
) : (
<>
<h1 className="font-cal mb-1 text-xl font-bold capitalize tracking-wide text-gray-900">
{props.heading}
</h1>
<p className="min-h-10 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">
{props.subtitle}
</p>
</>
)}
</div>
{props.CTA && <div className="mb-4 flex-shrink-0">{props.CTA}</div>}
</div>
@@ -342,7 +356,7 @@ const Layout = ({
"px-4 sm:px-6 md:px-8",
props.flexChildrenContainer && "flex flex-1 flex-col"
)}>
{props.children}
{!props.isLoading ? props.children : props.customLoader}
</div>
{/* show bottom navigation for md and smaller (tablet and phones) */}
{status === "authenticated" && (
@@ -403,6 +417,7 @@ type LayoutProps = {
// use when content needs to expand with flex
flexChildrenContainer?: boolean;
isPublic?: boolean;
customLoader?: ReactNode;
};
export default function Shell(props: LayoutProps) {
@@ -423,8 +438,10 @@ export default function Shell(props: LayoutProps) {
const i18n = useViewerI18n();
const { status } = useSession();
if (i18n.status === "loading" || query.status === "loading" || isRedirectingToOnboarding || loading) {
// show spinner whilst i18n is loading to avoid language flicker
const isLoading =
i18n.status === "loading" || query.status === "loading" || isRedirectingToOnboarding || loading;
if (isLoading) {
return (
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-50">
<Loader />
@@ -437,7 +454,7 @@ export default function Shell(props: LayoutProps) {
return (
<>
<CustomBranding lightVal={user?.brandColor} darkVal={user?.darkBrandColor} />
<MemoizedLayout plan={user?.plan} status={status} {...props} />
<MemoizedLayout plan={user?.plan} status={status} {...props} isLoading={isLoading} />
</>
);
}

View File

@@ -17,13 +17,13 @@ export function Tooltip({
}) {
return (
<TooltipPrimitive.Root
delayDuration={150}
delayDuration={50}
open={open}
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<TooltipPrimitive.Content
className="-mt-2 rounded-sm bg-black px-1 py-0.5 text-xs text-white shadow-lg"
className="slideInBottom -mt-2 rounded-sm bg-black px-1 py-0.5 text-xs text-white shadow-lg"
side="top"
align="center"
{...props}>

View File

@@ -15,7 +15,7 @@ export default function AppStoreCategories({
return (
<div className="mb-16">
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("popular_categories")}</h2>
<div className="grid-col-1 grid gap-3 md:grid-flow-col">
<div className="grid-col-1 grid w-full gap-3 overflow-scroll sm:grid-flow-col">
{categories.map((category) => (
<Link key={category.name} href={"/apps/categories/" + category.name}>
<a

View File

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

View File

@@ -29,8 +29,15 @@ export default function TwoFactor() {
<div className="mx-auto !mt-0 max-w-sm">
<p className="mb-4 text-sm text-gray-500">{t("2fa_enabled_instructions")}</p>
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
<div className="flex flex-row space-x-1">
<Input className={className} name="2fa1" inputMode="decimal" {...digits[0]} autoFocus />
<div className="flex flex-row justify-between">
<Input
className={className}
name="2fa1"
inputMode="decimal"
{...digits[0]}
autoFocus
autoComplete="one-time-code"
/>
<Input className={className} name="2fa2" inputMode="decimal" {...digits[1]} />
<Input className={className} name="2fa3" inputMode="decimal" {...digits[2]} />
<Input className={className} name="2fa4" inputMode="decimal" {...digits[3]} />

View File

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

View File

@@ -1,4 +1,4 @@
import { BanIcon, CheckIcon, ClockIcon, XIcon } from "@heroicons/react/outline";
import { BanIcon, CheckIcon, ClockIcon, XIcon, PencilAltIcon } from "@heroicons/react/outline";
import { PaperAirplaneIcon } from "@heroicons/react/outline";
import { BookingStatus } from "@prisma/client";
import dayjs from "dayjs";
@@ -87,11 +87,13 @@ function BookingListItem(booking: BookingItem) {
actions: [
{
id: "edit",
icon: PencilAltIcon,
label: t("edit_booking"),
href: `/reschedule/${booking.uid}`,
},
{
id: "reschedule_request",
icon: ClockIcon,
label: t("send_reschedule_request"),
onClick: () => setIsOpenRescheduleDialog(true),
},

View File

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

View File

@@ -18,13 +18,20 @@ import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { useEmbedStyles, useIsEmbed, useIsBackgroundTransparent, sdkActionManager } from "@calcom/embed-core";
import {
useEmbedStyles,
useIsEmbed,
useIsBackgroundTransparent,
sdkActionManager,
useEmbedType,
useEmbedNonStylesConfig,
} from "@calcom/embed-core";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { BASE_URL } from "@lib/config/constants";
import { BASE_URL, WEBAPP_URL } from "@lib/config/constants";
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
import useTheme from "@lib/hooks/useTheme";
import { isBrandingHidden } from "@lib/isBrandingHidden";
@@ -56,6 +63,8 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
const { t, i18n } = useLocale();
const { contracts } = useContracts();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
let isBackgroundTransparent = useIsBackgroundTransparent();
useExposePlanGlobally(plan);
useEffect(() => {
@@ -146,23 +155,24 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div>
<main
className={
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed
? classNames("m-auto", selectedDate ? "max-w-5xl" : "max-w-3xl")
? classNames(selectedDate ? "max-w-5xl" : "max-w-3xl")
: "transition-max-width mx-auto my-0 duration-500 ease-in-out md:my-24 " +
(selectedDate ? "max-w-5xl" : "max-w-3xl")
}>
(selectedDate ? "max-w-5xl" : "max-w-3xl")
)}>
{isReady && (
<div
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent ? "" : "bg-white dark:bg-gray-800 sm:dark:border-gray-600",
"border-bookinglightest rounded-sm md:border",
"border-bookinglightest rounded-md md:border",
isEmbed ? "mx-auto" : selectedDate ? "max-w-5xl" : "max-w-3xl"
)}>
{/* mobile: details */}
<div className="block p-4 sm:p-8 md:hidden">
<div className="block items-center sm:flex sm:space-x-4">
<div>
<AvatarGroup
border="border-2 dark:border-gray-800 border-white"
items={
@@ -180,20 +190,22 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
size={9}
truncateAfter={5}
/>
<div className="mt-4 sm:-mt-2">
<div className="mt-4">
<p className="text-sm font-medium text-black dark:text-white">{profile.name}</p>
<div className="mt-2 flex gap-2 text-xl font-medium dark:text-gray-100">
{eventType.title}
<div className="mt-2 gap-2 dark:text-gray-100">
<h1 className="text-bookingdark mb-4 text-xl font-semibold dark:text-white">
{eventType.title}
</h1>
{eventType?.description && (
<p className="mb-2 text-gray-600 dark:text-white">
<p className="text-bookinglight mb-2 dark:text-white">
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
{eventType.description}
</p>
)}
<div>
<ClockIcon className="mr-[10px] -mt-1 inline-block h-4 w-4" />
<p className="text-bookinglight mb-2 dark:text-white">
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
{eventType.length} {t("minutes")}
</div>
</p>
{eventType.price > 0 && (
<div className="text-gray-600 dark:text-white">
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 dark:text-gray-400" />
@@ -206,6 +218,22 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
</IntlProvider>
</div>
)}
<div className="md:hidden">
{booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-8 mb-2 text-gray-600 dark:text-white"
data-testid="former_time_p_mobile">
{t("former_time")}
</p>
<p className="text-gray-500 line-through dark:text-white">
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{typeof booking.startTime === "string" &&
parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)}
</div>
</div>
</div>
</div>
@@ -239,12 +267,12 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
{eventType.title}
</h1>
{eventType?.description && (
<p className="mb-2 text-gray-600 dark:text-white">
<p className="text-bookinglight mb-2 dark:text-white">
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
{eventType.description}
</p>
)}
<p className="mb-1 -ml-2 px-2 py-1 text-gray-600 dark:text-white">
<p className="text-bookinglight mb-2 dark:text-white">
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
{eventType.length} {t("minutes")}
</p>
@@ -262,7 +290,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
)}
<TimezoneDropdown />
{previousPage === `${BASE_URL}/${profile.slug}` && (
{previousPage === `${WEBAPP_URL}/${profile.slug}` && (
<div className="flex h-full flex-col justify-end">
<ArrowLeftIcon
className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white"
@@ -273,7 +301,9 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
)}
{booking?.startTime && rescheduleUid && (
<div>
<p className="mt-8 mb-2 text-gray-600 dark:text-white" data-testid="former_time_p">
<p
className="mt-4 mb-2 text-gray-600 dark:text-white"
data-testid="former_time_p_desktop">
{t("former_time")}
</p>
<p className="text-gray-500 line-through dark:text-white">
@@ -283,6 +313,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
</div>
)}
</div>
<DatePicker
date={selectedDate}
periodType={eventType?.periodType}

View File

@@ -5,6 +5,7 @@ import {
ExclamationIcon,
InformationCircleIcon,
} from "@heroicons/react/solid";
import { zodResolver } from "@hookform/resolvers/zod";
import { EventTypeCustomInputType } from "@prisma/client";
import { useContracts } from "contexts/contractsContext";
import dayjs from "dayjs";
@@ -17,8 +18,15 @@ import { Controller, useForm, useWatch } from "react-hook-form";
import { FormattedNumber, IntlProvider } from "react-intl";
import { ReactMultiEmail } from "react-multi-email";
import { useMutation } from "react-query";
import { z } from "zod";
import { useIsEmbed, useIsBackgroundTransparent } from "@calcom/embed-core";
import {
useIsEmbed,
useEmbedStyles,
useIsBackgroundTransparent,
useEmbedType,
useEmbedNonStylesConfig,
} from "@calcom/embed-core";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
@@ -41,6 +49,7 @@ import AvatarGroup from "@components/ui/AvatarGroup";
import type PhoneInputType from "@components/ui/form/PhoneInput";
import { BookPageProps } from "../../../pages/[user]/book";
import { HashLinkPageProps } from "../../../pages/d/[link]/book";
import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
/** These are like 40kb that not every user needs */
@@ -48,7 +57,7 @@ const PhoneInput = dynamic(
() => import("@components/ui/form/PhoneInput")
) as unknown as typeof PhoneInputType;
type BookingPageProps = BookPageProps | TeamBookingPageProps;
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
type BookingFormValues = {
name: string;
@@ -68,9 +77,13 @@ const BookingPage = ({
profile,
isDynamicGroupBooking,
locationLabels,
hasHashedBookingLink,
hashedLink,
}: BookingPageProps) => {
const { t, i18n } = useLocale();
const isEmbed = useIsEmbed();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const router = useRouter();
const { contracts } = useContracts();
const { data: session } = useSession();
@@ -187,8 +200,16 @@ const BookingPage = ({
};
};
const bookingFormSchema = z
.object({
name: z.string().min(1),
email: z.string().email(),
})
.passthrough();
const bookingForm = useForm<BookingFormValues>({
defaultValues: defaultValues(),
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
});
const selectedLocation = useWatch({
@@ -272,6 +293,8 @@ const BookingPage = ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))!.label,
value: booking.customInputs![inputId],
})),
hasHashedBookingLink,
hashedLink,
});
};
@@ -298,16 +321,17 @@ const BookingPage = ({
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<main
className={classNames(
isEmbed ? "mx-auto" : "mx-auto my-0 rounded-sm sm:my-24",
"max-w-3xl sm:border sm:dark:border-gray-600"
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "" : "sm:my-24",
"my-0 max-w-3xl "
)}>
{isReady && (
<div
className={classNames(
"overflow-hidden",
"main overflow-hidden",
isEmbed ? "" : "border border-gray-200",
isBackgroundTransparent ? "" : "bg-white dark:border-0 dark:bg-gray-800",
"sm:rounded-sm"
isBackgroundTransparent ? "" : "dark:border-1 bg-white dark:bg-gray-800",
"rounded-md sm:border sm:dark:border-gray-600"
)}>
<div className="px-4 py-5 sm:flex sm:p-4">
<div className="sm:w-1/2 sm:border-r sm:dark:border-gray-700">
@@ -372,7 +396,7 @@ const BookingPage = ({
</div>
)}
</div>
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
<div className="mt-8 sm:w-1/2 sm:pl-8 sm:pr-4">
<Form form={bookingForm} handleSubmit={bookEvent}>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
@@ -380,7 +404,7 @@ const BookingPage = ({
</label>
<div className="mt-1">
<input
{...bookingForm.register("name")}
{...bookingForm.register("name", { required: true })}
type="text"
name="name"
id="name"
@@ -427,7 +451,6 @@ const BookingPage = ({
{...bookingForm.register("locationType", { required: true })}
value={location.type}
defaultChecked={selectedLocation === location.type}
disabled={disableInput}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-gray-500">
{locationLabels[location.type]}

View File

@@ -56,23 +56,32 @@ export default function CreateEventTypeButton(props: Props) {
: undefined;
const pageSlug = router.query.eventPage || props.options[0].slug;
const hasTeams = !!props.options.find((option) => option.teamId);
const title: string =
typeof router.query.title === "string" && router.query.title ? router.query.title : "";
const length: number =
typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15;
const description: string =
typeof router.query.description === "string" && router.query.description ? router.query.description : "";
const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : "";
const type: string = typeof router.query.type == "string" && router.query.type ? router.query.type : "";
const form = useForm<z.infer<typeof createEventTypeInput>>({
resolver: zodResolver(createEventTypeInput),
});
const { setValue, watch, register } = form;
setValue("title", title);
setValue("length", length);
setValue("description", description);
setValue("slug", slug);
useEffect(() => {
if (!router.isReady) return;
const title: string =
typeof router.query.title === "string" && router.query.title ? router.query.title : "";
const length: number =
typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15;
const description: string =
typeof router.query.description === "string" && router.query.description
? router.query.description
: "";
const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : "";
setValue("title", title);
setValue("length", length);
setValue("description", description);
setValue("slug", slug);
// If query params change, update the form
}, [router.isReady, router.query, setValue]);
useEffect(() => {
const subscription = watch((value, { name, type }) => {
@@ -86,7 +95,7 @@ export default function CreateEventTypeButton(props: Props) {
const createMutation = trpc.useMutation("viewer.eventTypes.create", {
onSuccess: async ({ eventType }) => {
await router.push("/event-types/" + eventType.id);
await router.replace("/event-types/" + eventType.id);
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
},
onError: (err) => {

View File

@@ -0,0 +1,63 @@
import { LinkIcon } from "@heroicons/react/outline";
import { ClockIcon, DotsHorizontalIcon, ExternalLinkIcon, UserIcon } from "@heroicons/react/solid";
import React from "react";
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
function SkeletonLoader() {
return (
<SkeletonContainer>
<div className="mb-4 flex items-center">
<SkeletonAvatar width="8" height="8"></SkeletonAvatar>
<div className="space-y-1">
<SkeletonText height="4" width="16"></SkeletonText>
<SkeletonText height="4" width="24"></SkeletonText>
</div>
</div>
<ul className="divide-y divide-neutral-200 border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
<SkeletonItem />
<SkeletonItem />
<SkeletonItem />
</ul>
</SkeletonContainer>
);
}
export default SkeletonLoader;
function SkeletonItem() {
return (
<li className="group flex w-full items-center justify-between px-4 py-4 sm:px-6">
<div className="flex-grow truncate text-sm">
<div>
<SkeletonText width="32" height="5"></SkeletonText>
</div>
<div className="">
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
<li className="flex items-center whitespace-nowrap">
<ClockIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200"></ClockIcon>
<SkeletonText width="12" height="4"></SkeletonText>
</li>
<li className="flex items-center whitespace-nowrap">
<UserIcon className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200"></UserIcon>
<SkeletonText width="16" height="4"></SkeletonText>
</li>
</ul>
</div>
</div>
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
<div className="flex justify-between rtl:space-x-reverse">
<div className="btn-icon appearance-none">
<ExternalLinkIcon className="h-5 w-5" />
</div>
<div className="btn-icon appearance-none">
<LinkIcon className="h-5 w-5" />
</div>
<div className="btn-icon appearance-none">
<DotsHorizontalIcon className="h-5 w-5" />
</div>
</div>
</div>
</li>
);
}

View File

@@ -139,7 +139,7 @@ function ConnectedCalendarsList(props: Props) {
) : (
<Alert
severity="warning"
title="Something went wrong"
title={t("calendar_error")}
message={item.error?.message}
actions={
<DisconnectIntegration

View File

@@ -56,11 +56,11 @@ const ChangePasswordSection = () => {
return (
<>
<div className="mt-6">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
</div>
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
<div className="py-6 lg:pb-8">
<div className="py-6 lg:pb-5">
<div className="my-3">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("change_password")}</h2>
</div>
<div className="flex">
<div className="w-1/2 ltr:mr-2 rtl:ml-2">
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
@@ -99,9 +99,10 @@ const ChangePasswordSection = () => {
</div>
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
<div className="flex justify-end py-8">
<Button type="submit">{t("save")}</Button>
<Button color="secondary" type="submit">
{t("save")}
</Button>
</div>
<hr className="mt-4" />
</div>
</form>
</>

View File

@@ -173,6 +173,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
inputMode="numeric"
onInput={(e) => setTotpCode(e.currentTarget.value)}
className="block w-full rounded-sm border-gray-300 shadow-sm sm:text-sm"
autoComplete="one-time-code"
/>
</div>

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
import { useSession } from "next-auth/react";
import { Trans } from "next-i18next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert } from "@calcom/ui/Alert";
type Props = {};
function ImpersonatingBanner({}: Props) {
const { t } = useLocale();
const { data } = useSession();
if (!data?.user.impersonatedByUID) return null;
return (
<Alert
severity="warning"
title={
<>
{t("impersonating_user_warning", { user: data.user.username })}{" "}
<Trans i18nKey="impersonating_stop_instructions">
<a href="/auth/logout" className="underline">
Click Here To stop
</a>
.
</Trans>
</>
}
className="mx-4 mb-2 sm:mx-6 md:mx-8"
/>
);
}
export default ImpersonatingBanner;

View File

@@ -22,7 +22,7 @@ export default function ModalContainer(props: Props) {
{
"sm:w-full sm:max-w-lg ": !props.wide,
"sm:w-4xl sm:max-w-4xl": props.wide,
"overflow-scroll": props.scroll,
"overflow-auto": props.scroll,
"!p-0": props.noPadding,
}
)}>

View File

@@ -52,6 +52,12 @@ const DropdownActions = ({ actions, actionTrigger }: { actions: ActionType[]; ac
};
const TableActions: FC<Props> = ({ actions }) => {
const mobileActions = actions.flatMap((action) => {
if (action.actions) {
return action.actions;
}
return action;
});
return (
<>
<div className="hidden space-x-2 rtl:space-x-reverse lg:block">
@@ -72,12 +78,11 @@ const TableActions: FC<Props> = ({ actions }) => {
if (!action.actions) {
return button;
}
return <DropdownActions key={action.id} actions={action.actions} actionTrigger={button} />;
})}
</div>
<div className="inline-block text-left lg:hidden">
<DropdownActions actions={actions} />
<DropdownActions actions={mobileActions} />
</div>
</>
);

View File

@@ -10,9 +10,11 @@ type Props = {
date: Date;
onDatesChange?: ((date: Date) => void) | undefined;
className?: string;
disabled?: boolean;
minDate?: Date;
};
export const DatePicker = ({ date, onDatesChange, className }: Props) => {
export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }: Props) => {
return (
<PrimitiveDatePicker
className={classNames(
@@ -22,6 +24,8 @@ export const DatePicker = ({ date, onDatesChange, className }: Props) => {
clearIcon={null}
calendarIcon={<CalendarIcon className="h-5 w-5 text-gray-500" />}
value={date}
minDate={minDate}
disabled={disabled}
onChange={onDatesChange}
/>
);

View File

@@ -20,7 +20,7 @@ export default function LicenseBanner() {
- Acquire a commercial license to remove these terms by visiting: cal.com/sales
NEXT_PUBLIC_LICENSE_CONSENT=''
*/
if (process.env.NEXT_PUBLIC_LICENSE_CONSENT === "agree") {
if (process.env.NEXT_PUBLIC_LICENSE_CONSENT === "agree" || process.env.NEXT_PUBLIC_IS_E2E) {
return null;
}

View File

@@ -0,0 +1,150 @@
import { ClipboardCopyIcon } from "@heroicons/react/solid";
import dayjs from "dayjs";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import Button from "@calcom/ui/Button";
import { DialogFooter } from "@calcom/ui/Dialog";
import Switch from "@calcom/ui/Switch";
import { Form, TextField } from "@calcom/ui/form/fields";
import { trpc } from "@lib/trpc";
import { Tooltip } from "@components/Tooltip";
import { DatePicker } from "@components/ui/form/DatePicker";
import { TApiKeys } from "./ApiKeyListItem";
export default function ApiKeyDialogForm(props: {
title: string;
defaultValues?: Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean };
handleClose: () => void;
}) {
const { t } = useLocale();
const utils = trpc.useContext();
const {
defaultValues = {
note: "",
neverExpires: false,
expiresAt: dayjs().add(1, "month").toDate(),
},
} = props;
const [apiKey, setApiKey] = useState("");
const [successfulNewApiKeyModal, setSuccessfulNewApiKeyModal] = useState(false);
const [apiKeyDetails, setApiKeyDetails] = useState({
id: "",
hashedKey: "",
expiresAt: null as Date | null,
note: "" as string | null,
neverExpires: false,
});
const form = useForm({
defaultValues,
});
const watchNeverExpires = form.watch("neverExpires");
return (
<>
{successfulNewApiKeyModal ? (
<>
<div className="mb-10">
<h2 className="font-semi-bold font-cal mb-2 text-xl tracking-wide text-gray-900">
{apiKeyDetails ? t("success_api_key_edited") : t("success_api_key_created")}
</h2>
<div className="text-sm text-gray-900">
<span className="font-semibold">{t("success_api_key_created_bold_tagline")}</span>{" "}
{t("you_will_only_view_it_once")}
</div>
</div>
<div>
<div className="flex">
<code className="my-2 mr-1 w-full truncate rounded-sm bg-gray-100 py-2 px-3 align-middle font-mono text-gray-800">
{apiKey}
</code>
<Tooltip content={t("copy_to_clipboard")}>
<Button
onClick={() => {
navigator.clipboard.writeText(apiKey);
showToast(t("api_key_copied"), "success");
}}
type="button"
className=" my-2 px-4 text-base">
<ClipboardCopyIcon className="mr-2 h-5 w-5 text-neutral-100" />
{t("copy")}
</Button>
</Tooltip>
</div>
<span className="text-sm text-gray-400">
{apiKeyDetails.neverExpires
? t("never_expire_key")
: `${t("expires")} ${apiKeyDetails?.expiresAt?.toLocaleDateString()}`}
</span>
</div>
<DialogFooter>
<Button type="button" color="secondary" onClick={props.handleClose} tabIndex={-1}>
{t("done")}
</Button>
</DialogFooter>
</>
) : (
<Form<Omit<TApiKeys, "userId" | "createdAt" | "lastUsedAt"> & { neverExpires: boolean }>
form={form}
handleSubmit={async (event) => {
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
setApiKey(apiKey);
setApiKeyDetails({ ...event });
await utils.invalidateQueries(["viewer.apiKeys.list"]);
setSuccessfulNewApiKeyModal(true);
}}
className="space-y-4">
<div className=" mb-10 mt-1">
<h2 className="font-semi-bold font-cal text-xl tracking-wide text-gray-900">{props.title}</h2>
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_key_modal_subtitle")}</p>
</div>
<TextField
label={t("personal_note")}
placeholder={t("personal_note_placeholder")}
{...form.register("note")}
type="text"
/>
<div className="flex flex-col">
<div className="flex justify-between py-2">
<span className="block text-sm font-medium text-gray-700">{t("expire_date")}</span>
<Controller
name="neverExpires"
render={({ field: { onChange, value } }) => (
<Switch label={t("never_expire_key")} onCheckedChange={onChange} checked={value} />
)}
/>
</div>
<Controller
name="expiresAt"
render={({ field: { onChange, value } }) => (
<DatePicker
disabled={watchNeverExpires}
minDate={new Date()}
date={value}
onDatesChange={onChange}
/>
)}
/>
</div>
<DialogFooter>
<Button type="button" color="secondary" onClick={props.handleClose} tabIndex={-1}>
{t("cancel")}
</Button>
<Button type="submit" loading={form.formState.isSubmitting}>
{apiKeyDetails ? t("save") : t("create")}
</Button>
</DialogFooter>
</Form>
)}
</>
);
}

View File

@@ -0,0 +1,77 @@
import { PlusIcon } from "@heroicons/react/outline";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import ApiKeyDialogForm from "@ee/components/apiKeys/ApiKeyDialogForm";
import ApiKeyListItem, { TApiKeys } from "@ee/components/apiKeys/ApiKeyListItem";
import { QueryCell } from "@lib/QueryCell";
import { trpc } from "@lib/trpc";
import { List } from "@components/List";
export default function ApiKeyListContainer() {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.apiKeys.list"]);
const [newApiKeyModal, setNewApiKeyModal] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires: boolean }) | null>(null);
return (
<QueryCell
query={query}
success={({ data }) => (
<>
<div className="flex flex-row justify-between truncate pl-2 pr-1 ">
<div className="mt-9">
<h2 className="font-cal text-lg font-medium leading-6 text-gray-900">{t("api_keys")}</h2>
<p className="mt-1 mb-5 text-sm text-gray-500">{t("api_keys_subtitle")}</p>
</div>
<div className="self-center">
<Button StartIcon={PlusIcon} color="secondary" onClick={() => setNewApiKeyModal(true)}>
{t("generate_new_api_key")}
</Button>
</div>
</div>
{data.length > 0 && (
<List className="pb-6">
{data.map((item: any) => (
<ApiKeyListItem
key={item.id}
apiKey={item}
onEditApiKey={() => {
setApiKeyToEdit(item);
setEditModalOpen(true);
}}
/>
))}
</List>
)}
{/* New api key dialog */}
<Dialog open={newApiKeyModal} onOpenChange={(isOpen) => !isOpen && setNewApiKeyModal(false)}>
<DialogContent>
<ApiKeyDialogForm title={t("create_api_key")} handleClose={() => setNewApiKeyModal(false)} />
</DialogContent>
</Dialog>
{/* Edit api key dialog */}
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
<DialogContent>
{apiKeyToEdit && (
<ApiKeyDialogForm
title={t("edit_api_key")}
key={apiKeyToEdit.id}
handleClose={() => setEditModalOpen(false)}
defaultValues={apiKeyToEdit}
/>
)}
</DialogContent>
</Dialog>
</>
)}
/>
);
}

View File

@@ -0,0 +1,107 @@
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
import { ExclamationIcon } from "@heroicons/react/solid";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { inferQueryOutput, trpc } from "@lib/trpc";
import { ListItem } from "@components/List";
import { Tooltip } from "@components/Tooltip";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Badge from "@components/ui/Badge";
dayjs.extend(relativeTime);
export type TApiKeys = inferQueryOutput<"viewer.apiKeys.list">[number];
export default function ApiKeyListItem(props: { apiKey: TApiKeys; onEditApiKey: () => void }) {
const { t } = useLocale();
const utils = trpc.useContext();
const isExpired = props?.apiKey?.expiresAt ? props.apiKey.expiresAt < new Date() : null;
const neverExpires = props?.apiKey?.expiresAt === null;
const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete", {
async onSuccess() {
await utils.invalidateQueries(["viewer.apiKeys.list"]);
},
});
return (
<ListItem className="-mt-px flex w-full p-4">
<div className="flex w-full justify-between">
<div className="flex max-w-full flex-col truncate">
<div className="flex space-x-2">
<span className="text-gray-900">
{props?.apiKey?.note ? props.apiKey.note : t("api_key_no_note")}
</span>
{!neverExpires && isExpired && (
<Badge className="-p-2" variant="default">
{t("expired")}
</Badge>
)}
</div>
<div className="mt-2 flex">
<span
className={classNames(
"flex flex-col space-x-2 space-y-1 text-xs sm:flex-row sm:space-y-0 sm:rtl:space-x-reverse",
isExpired ? "text-red-600" : "text-gray-500",
neverExpires ? "text-yellow-600" : ""
)}>
{neverExpires ? (
<div className="flex flex-row space-x-3 text-gray-500">
<ExclamationIcon className="w-4" />
{t("api_key_never_expires")}
</div>
) : (
`${isExpired ? t("expired") : t("expires")} ${dayjs(
props?.apiKey?.expiresAt?.toString()
).fromNow()}`
)}
</span>
</div>
</div>
<div className="flex">
<Tooltip content={t("edit_api_key")}>
<Button
onClick={() => props.onEditApiKey()}
color="minimal"
size="icon"
StartIcon={PencilAltIcon}
className="ml-4 w-full self-center p-2"
/>
</Tooltip>
<Dialog>
<Tooltip content={t("delete_api_key")}>
<DialogTrigger asChild>
<Button
onClick={(e) => {
e.stopPropagation();
}}
color="minimal"
size="icon"
StartIcon={TrashIcon}
className="ml-2 w-full self-center p-2"
/>
</DialogTrigger>
</Tooltip>
<ConfirmationDialogContent
variety="danger"
title={t("confirm_delete_api_key")}
confirmBtnText={t("revoke_api_key")}
cancelBtnText={t("cancel")}
onConfirm={() =>
deleteApiKey.mutate({
id: props.apiKey.id,
})
}>
{t("delete_api_key_confirm_title")}
</ConfirmationDialogContent>
</Dialog>
</div>
</div>
</ListItem>
);
}

View File

@@ -1,5 +1,6 @@
import { CreditCardIcon } from "@heroicons/react/solid";
import { Elements } from "@stripe/react-stripe-js";
import classNames from "classnames";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import toArray from "dayjs/plugin/toArray";
@@ -8,6 +9,7 @@ import Head from "next/head";
import React, { FC, useEffect, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core";
import getStripe from "@calcom/stripe/client";
import PaymentComponent from "@ee/components/stripe/Payment";
import { PaymentPageProps } from "@ee/pages/payment/[uid]";
@@ -26,16 +28,33 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
const [is24h, setIs24h] = useState(isBrowserLocale24h());
const [date, setDate] = useState(dayjs.utc(props.booking.startTime));
const { isReady, Theme } = useTheme(props.profile.theme);
const isEmbed = useIsEmbed();
useEffect(() => {
let embedIframeWidth = 0;
setDate(date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()));
setIs24h(!!localStorage.getItem("timeOption.is24hClock"));
}, []);
if (isEmbed) {
requestAnimationFrame(function fixStripeIframe() {
// HACK: Look for stripe iframe and center position it just above the embed content
const stripeIframeWrapper = document.querySelector(
'iframe[src*="https://js.stripe.com/v3/authorize-with-url-inner"]'
)?.parentElement;
if (stripeIframeWrapper) {
stripeIframeWrapper.style.margin = "0 auto";
stripeIframeWrapper.style.width = embedIframeWidth + "px";
}
requestAnimationFrame(fixStripeIframe);
});
sdkActionManager?.on("__dimensionChanged", (e) => {
embedIframeWidth = e.detail.data.iframeWidth as number;
});
}
}, [isEmbed]);
const eventName = props.booking.title;
return isReady ? (
<div className="h-screen bg-neutral-50 dark:bg-neutral-900">
<div className="h-screen">
<Theme />
<Head>
<title>
@@ -51,7 +70,10 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
&#8203;
</span>
<div
className="inline-block transform overflow-hidden rounded-sm border border-neutral-200 bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 dark:bg-gray-800 sm:my-8 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle"
className={classNames(
"main inline-block transform overflow-hidden rounded-lg border border-neutral-200 bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 dark:bg-gray-800 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle",
isEmbed ? "" : "sm:my-8"
)}
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">

View File

@@ -80,7 +80,7 @@ export default function TeamAvailabilityModal(props: Props) {
</div>
{props.team && props.member && (
<TeamAvailabilityTimes
className="overflow-scroll"
className="overflow-auto"
teamId={props.team.id}
memberId={props.member.id}
frequency={frequency}

View File

@@ -0,0 +1,62 @@
import CredentialsProvider from "next-auth/providers/credentials";
import { getSession } from "next-auth/react";
import prisma from "@lib/prisma";
const ImpersonationProvider = CredentialsProvider({
id: "impersonation-auth",
name: "Impersonation",
type: "credentials",
credentials: {
username: { label: "Username", type: "text " },
},
async authorize(creds, req) {
// @ts-ignore need to figure out how to correctly type this
const session = await getSession({ req });
if (session?.user.role !== "ADMIN") {
throw new Error("You do not have permission to do this.");
}
if (session?.user.username === creds?.username) {
throw new Error("You cannot impersonate yourself.");
}
const user = await prisma.user.findUnique({
where: {
username: creds?.username,
},
});
if (!user) {
throw new Error("This user does not exist");
}
// Log impersonations for audit purposes
await prisma.impersonations.create({
data: {
impersonatedBy: {
connect: {
id: session.user.id,
},
},
impersonatedUser: {
connect: {
id: user.id,
},
},
},
});
const obj = {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: user.role,
impersonatedByUID: session?.user.id,
};
return obj;
},
});
export default ImpersonationProvider;

View File

@@ -32,7 +32,9 @@ async function handlePaymentSuccess(event: Stripe.Event) {
bookingId: true,
},
});
if (!payment?.bookingId) {
console.log(JSON.stringify(paymentIntent), JSON.stringify(payment));
}
if (!payment?.bookingId) throw new Error("Payment not found");
const booking = await prisma.booking.findUnique({
@@ -172,6 +174,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET);
if (event.account) {
throw new HttpCode({ statusCode: 202, message: "Incoming connected account" });
}
const handler = webhookHandlers[event.type];
if (handler) {
await handler(event);

View File

@@ -1,3 +1,4 @@
import React, { ReactNode } from "react";
import {
QueryObserverIdleResult,
QueryObserverLoadingErrorResult,
@@ -31,6 +32,7 @@ type JSXElementOrNull = JSX.Element | null;
interface QueryCellOptionsBase<TData, TError extends ErrorLike> {
query: UseQueryResult<TData, TError>;
customLoader?: ReactNode;
error?: (
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
) => JSXElementOrNull;
@@ -62,7 +64,6 @@ export function QueryCell<TData, TError extends ErrorLike>(
opts: QueryCellOptionsNoEmpty<TData, TError> | QueryCellOptionsWithEmpty<TData, TError>
) {
const { query } = opts;
if (query.status === "success") {
if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) {
return opts.empty(query);
@@ -76,11 +77,13 @@ export function QueryCell<TData, TError extends ErrorLike>(
)
);
}
const StatusLoader = opts.customLoader || <Loader />; // Fixes edge case where this can return null form query cell
if (query.status === "loading") {
return opts.loading?.(query) ?? <Loader />;
return opts.loading?.(query) ?? StatusLoader;
}
if (query.status === "idle") {
return opts.idle?.(query) ?? <Loader />;
return opts.idle?.(query) ?? StatusLoader;
}
// impossible state
return null;
@@ -108,6 +111,7 @@ const withQuery = <TPath extends keyof TQueryValues & string>(
>
) {
const query = trpc.useQuery(pathAndInput, params);
return <QueryCell query={query} {...opts} />;
};
};

View File

@@ -1,6 +1,6 @@
import { Attendee } from "@prisma/client";
import { TFunction } from "next-i18next";
import { Attendee } from "@calcom/prisma/client";
import { Person } from "@calcom/types/Calendar";
export const attendeeToPersonConversionType = (attendees: Attendee[], t: TFunction): Person[] => {

View File

@@ -0,0 +1,85 @@
import { Account, IdentityProvider, Prisma, PrismaClient, User, VerificationToken } from "@prisma/client";
import { identityProviderNameMap } from "@lib/auth";
/** @return { import("next-auth/adapters").Adapter } */
export default function CalComAdapter(prismaClient: PrismaClient) {
return {
createUser: (data: Prisma.UserCreateInput) => prismaClient.user.create({ data }),
getUser: (id: User["id"]) => prismaClient.user.findUnique({ where: { id } }),
getUserByEmail: (email: User["email"]) => prismaClient.user.findUnique({ where: { email } }),
async getUserByAccount(provider_providerAccountId: {
providerAccountId: Account["providerAccountId"];
provider: User["identityProvider"];
}) {
let _account;
const account = await prismaClient.account.findUnique({
where: {
provider_providerAccountId,
},
select: { user: true },
});
if (account) {
return (_account = account === null || account === void 0 ? void 0 : account.user) !== null &&
_account !== void 0
? _account
: null;
}
// NOTE: this code it's our fallback to users without Account but credentials in User Table
// We should remove this code after all googles tokens have expired
const provider = provider_providerAccountId?.provider.toUpperCase() as IdentityProvider;
if (["GOOGLE", "SAML"].indexOf(provider) < 0) {
return null;
}
const obtainProvider = identityProviderNameMap[provider].toUpperCase() as IdentityProvider;
const user = await prismaClient.user.findFirst({
where: {
identityProviderId: provider_providerAccountId?.providerAccountId,
identityProvider: obtainProvider,
},
});
return user || null;
},
updateUser: ({ id, ...data }: Prisma.UserUncheckedCreateInput) =>
prismaClient.user.update({ where: { id }, data }),
deleteUser: (id: User["id"]) => prismaClient.user.delete({ where: { id } }),
async createVerificationToken(data: VerificationToken) {
const { id: _, ...verificationToken } = await prismaClient.verificationToken.create({
data,
});
return verificationToken;
},
async useVerificationToken(identifier_token: Prisma.VerificationTokenIdentifierTokenCompoundUniqueInput) {
try {
const { id: _, ...verificationToken } = await prismaClient.verificationToken.delete({
where: { identifier_token },
});
return verificationToken;
} catch (error) {
// If token already used/deleted, just return null
// https://www.prisma.io/docs/reference/api-reference/error-reference#p2025
// @ts-ignore
if (error.code === "P2025") return null;
throw error;
}
},
linkAccount: (data: Prisma.AccountCreateInput) => prismaClient.account.create({ data }),
// @NOTE: All methods below here are not being used but leaved if they are required
unlinkAccount: (provider_providerAccountId: Prisma.AccountProviderProviderAccountIdCompoundUniqueInput) =>
prismaClient.account.delete({ where: { provider_providerAccountId } }),
async getSessionAndUser(sessionToken: string) {
const userAndSession = await prismaClient.session.findUnique({
where: { sessionToken },
include: { user: true },
});
if (!userAndSession) return null;
const { user, ...session } = userAndSession;
return { user, session };
},
createSession: (data: Prisma.SessionCreateInput) => prismaClient.session.create({ data }),
updateSession: (data: Prisma.SessionWhereUniqueInput) =>
prismaClient.session.update({ where: { sessionToken: data.sessionToken }, data }),
deleteSession: (sessionToken: string) => prismaClient.session.delete({ where: { sessionToken } }),
};
}

View File

@@ -6,7 +6,6 @@ import utc from "dayjs/plugin/utc";
import { createEvent, DateArray, Person } from "ics";
import { getCancelLink } from "@calcom/lib/CalEventParser";
import { Attendee } from "@calcom/prisma/client";
import { CalendarEvent } from "@calcom/types/Calendar";
import {

View File

@@ -9,10 +9,9 @@ import nodemailer from "nodemailer";
import { getAppName } from "@calcom/app-store/utils";
import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { serverConfig } from "@calcom/lib/serverConfig";
import type { Person, CalendarEvent } from "@calcom/types/Calendar";
import { serverConfig } from "@lib/serverConfig";
import {
emailHead,
emailSchedulingBodyHeader,
@@ -321,7 +320,8 @@ ${getRichDescription(this.calEvent)}
}
protected getLocation(): string {
let providerName = this.calEvent.location ? getAppName(this.calEvent.location) : "";
let providerName = this.calEvent.location && getAppName(this.calEvent.location);
if (this.calEvent.location && this.calEvent.location.includes("integrations:")) {
const location = this.calEvent.location.split(":")[1];
providerName = location[0].toUpperCase() + location.slice(1);

View File

@@ -0,0 +1,573 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!-- <head> -->
<title>${headerContent}</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Roboto:400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
<!-- </head> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<title></title>
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url('https://fonts.googleapis.com/css?family=Inter:400,700&display=swap');
#outlook a {
padding: 0;
}
body {
width: 100% !important;
height: 100%;
-webkit-text-size-adjust: none;
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
word-break: break-word;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
a {
color: #3b82f6;
}
a img {
border: none;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
/* Type ------------------------------ */
body,
td,
th {
font-family: 'Roboto', Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
font-size: 22px;
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #333333;
font-size: 16px;
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #333333;
font-size: 14px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p,
ul,
ol,
blockquote {
margin: 0.4em 0 1.1875em;
font-size: 16px;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
/* Utilities ------------------------------ */
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
/* Buttons ------------------------------ */
.button {
background-color: #000;
border-top: 10px solid #000;
border-right: 18px solid #000;
border-bottom: 10px solid #000;
border-left: 18px solid #000;
display: inline-block;
color: #fff !important;
text-decoration: none;
border-radius: 0;
/* box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); */
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
/* Attribute list ------------------------------ */
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #f4f4f7;
padding: 16px;
}
.attributes_item {
padding: 0;
}
/* Related Items ------------------------------ */
.related {
width: 100%;
margin: 0;
padding: 25px 0 0 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #cbcccf;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: 0.5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #cbcccf;
text-align: center;
padding: 25px 0 10px;
}
/* Data table ------------------------------ */
body {
background-color: #f2f4f6;
color: #51545e;
}
p {
color: #51545e;
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #f2f4f6;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
color: #a8aaaf;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
background-color: #ffffff;
}
.email-footer {
width: 570px;
margin: 0 auto;
padding: 0;
-premailer-width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #a8aaaf;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
-premailer-width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #eaeaec;
}
.content-cell {
padding: 45px;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body_inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #333333 !important;
color: #fff !important;
}
p,
ul,
ol,
blockquote,
h1,
h2,
h3,
span,
.purchase_item {
color: #fff !important;
}
.attributes_content {
background-color: #222 !important;
}
.email-masthead_name {
text-shadow: none !important;
}
}
:root {
color-scheme: light dark;
supported-color-schemes: light dark;
}
</style>
<!--[if mso]>
<style type="text/css">
.f-fallback {
font-family: Arial, sans-serif;
}
</style>
<![endif]-->
</head>
<body>
<span class="preheader">This link will expire in 10 min.</span>
<table
class="email-wrapper"
width="100%"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tr>
<td align="center">
<table
class="email-content"
width="100%"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<!-- <tr>
<td class="email-masthead">
<a href="{{base_url}}" class="f-fallback email-masthead_name">
Cal.com
</a>
</td>
</tr> -->
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;padding-top:32px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:89px;">
<a href="{{base_url}}" target="_blank">
<img height="19" src="https://app.cal.com/emails/CalLogo@2x.png" style="border:0;display:block;outline:none;text-decoration:none;height:19px;width:100%;font-size:13px;" width="89" />
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!-- Email Body -->
<tr>
<td
class="email-body"
width="570"
cellpadding="0"
cellspacing="0"
>
<table
class="email-body_inner"
align="center"
width="570"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<!-- Body content -->
<tr>
<td class="content-cell">
<div class="f-fallback">
<p>
Click the button below to log in to Cal.com<br />
This link will expire in 10 minutes.
</p>
<!-- Action -->
<table
class="body-action"
align="center"
width="100%"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tr>
<td align="center">
<!-- Border based button
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table
width="100%"
border="0"
cellspacing="0"
cellpadding="0"
role="presentation"
>
<tr>
<td>
<a
href="{{signin_url}}"
class="f-fallback button"
target="_blank"
>Log into Cal.com</a
>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>
Confirming this request will securely log you in using
{{email}}.
</p>
<p>Enjoy your new scheduling soultion by,<br />The Cal.com Team</p>
<!-- Sub copy -->
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">
If youre having trouble with the button above,
copy and paste the URL below into your web
browser.
</p>
<p class="f-fallback sub">{{signin_url}}</p>
</td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table
class="email-footer"
align="center"
width="570"
cellpadding="0"
cellspacing="0"
role="presentation"
>
<tr>
<td class="content-cell" align="center">
<p class="f-fallback sub align-center">
&copy; 2022 Cal.com. All rights reserved.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -2,8 +2,7 @@ import { TFunction } from "next-i18next";
import nodemailer from "nodemailer";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { serverConfig } from "@lib/serverConfig";
import { serverConfig } from "@calcom/lib/serverConfig";
import { emailHead, linkIcon, emailBodyLogo } from "./common";

View File

@@ -9,10 +9,9 @@ import nodemailer from "nodemailer";
import { getAppName } from "@calcom/app-store/utils";
import { getCancelLink, getRichDescription } from "@calcom/lib/CalEventParser";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { serverConfig } from "@calcom/lib/serverConfig";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { serverConfig } from "@lib/serverConfig";
import {
emailHead,
emailSchedulingBodyHeader,
@@ -314,7 +313,7 @@ ${getRichDescription(this.calEvent)}
}
protected getLocation(): string {
let providerName = this.calEvent.location ? getAppName(this.calEvent.location) : "";
let providerName = this.calEvent.location && getAppName(this.calEvent.location); // This returns null if nothing is found
if (this.calEvent.location && this.calEvent.location.includes("integrations:")) {
const location = this.calEvent.location.split(":")[1];

View File

@@ -2,8 +2,7 @@ import { TFunction } from "next-i18next";
import nodemailer from "nodemailer";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { serverConfig } from "@lib/serverConfig";
import { serverConfig } from "@calcom/lib/serverConfig";
import { emailHead, linkIcon, emailBodyLogo } from "./common";

View File

@@ -1,6 +1,4 @@
import { PrismaClient } from "@prisma/client";
import { Prisma } from "@calcom/prisma/client";
import { Prisma, PrismaClient } from "@prisma/client";
async function getBooking(prisma: PrismaClient, uid: string) {
const booking = await prisma.booking.findFirst({

View File

@@ -1,6 +1,4 @@
import { useEffect } from "react";
import { UserPlan } from "@calcom/prisma/client";
import { UserPlan } from "@prisma/client";
/**
* TODO: It should be exposed at a single place.

View File

@@ -18,6 +18,8 @@ function applyThemeAndAddListener(theme: string) {
document.documentElement.classList.remove("dark");
}
} else {
document.documentElement.classList.remove("dark");
document.documentElement.classList.remove("light");
document.documentElement.classList.add(theme);
}
};
@@ -33,15 +35,16 @@ export default function useTheme(theme?: Maybe<string>) {
const embedTheme = useEmbedTheme();
// Embed UI configuration takes more precedence over App Configuration
theme = embedTheme || theme;
const [_theme, setTheme] = useState<Maybe<string>>(null);
useEffect(() => {
// TODO: isReady doesn't seem required now. This is also impacting PSI Score for pages which are using isReady.
setIsReady(true);
setTheme(theme);
}, []);
function Theme() {
const code = applyThemeAndAddListener.toString();
const themeStr = theme ? `"${theme}"` : null;
const themeStr = _theme ? `"${_theme}"` : null;
return (
<Head>
<script dangerouslySetInnerHTML={{ __html: `(${code})(${themeStr})` }}></script>

View File

@@ -27,6 +27,8 @@ export type BookingCreateBody = {
metadata: {
[key: string]: string;
};
hasHashedBookingLink: boolean;
hashedLink?: string | null;
};
export type BookingResponse = Booking & {

View File

@@ -80,10 +80,14 @@ const nextConfig = {
source: "/:user/avatar.png",
destination: "/api/user/avatar?username=:user",
},
{
source: "/team/:teamname/avatar.png",
destination: "/api/user/avatar?teamname=:teamname",
},
];
},
async redirects() {
return [
const redirects = [
{
source: "/settings",
destination: "/settings/profile",
@@ -100,6 +104,28 @@ const nextConfig = {
permanent: false,
},
];
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
redirects.push(
{
source: "/apps/dailyvideo",
destination: "/apps/daily-video",
permanent: true,
},
{
source: "/apps/huddle01_video",
destination: "/apps/huddle01",
permanent: true,
},
{
source: "/apps/jitsi_video",
destination: "/apps/jitsi",
permanent: true,
}
);
}
return redirects;
},
};

View File

@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "1.4.1",
"version": "1.5.3",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -37,11 +37,12 @@
"@calcom/ui": "*",
"@daily-co/daily-js": "^0.21.0",
"@glidejs/glide": "^3.5.2",
"@heroicons/react": "^1.0.5",
"@heroicons/react": "^1.0.6",
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.8.5",
"@jitsu/sdk-js": "^2.2.4",
"@metamask/providers": "^8.1.1",
"@next-auth/prisma-adapter": "^1.0.3",
"@next/bundle-analyzer": "12.1.0",
"@radix-ui/react-avatar": "^0.1.0",
"@radix-ui/react-collapsible": "^0.1.0",
@@ -76,7 +77,7 @@
"micro": "^9.3.4",
"mime-types": "^2.1.35",
"next": "^12.1.0",
"next-auth": "^4.0.6",
"next-auth": "^4.3.3",
"next-i18next": "^8.9.0",
"next-mdx-remote": "^4.0.2",
"next-seo": "^4.26.0",

View File

@@ -1,6 +1,7 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
import { BadgeCheckIcon } from "@heroicons/react/solid";
import { UserPlan } from "@prisma/client";
import classNames from "classnames";
import { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic";
import Link from "next/link";
@@ -9,9 +10,10 @@ import React, { useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import { JSONObject } from "superjson/dist/types";
import { sdkActionManager, useEmbedStyles, useIsEmbed } from "@calcom/embed-core";
import { sdkActionManager, useEmbedNonStylesConfig, useEmbedStyles, useIsEmbed } from "@calcom/embed-core";
import defaultEvents, {
getDynamicEventDescription,
getGroupName,
getUsernameList,
getUsernameSlugLink,
} from "@calcom/lib/defaultEvents";
@@ -23,6 +25,7 @@ import prisma from "@lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CustomBranding from "@components/CustomBranding";
import AvatarGroup from "@components/ui/AvatarGroup";
import { AvatarSSR } from "@components/ui/AvatarSSR";
@@ -37,7 +40,7 @@ interface EvtsToVerify {
}
export default function User(props: inferSSRProps<typeof getServerSideProps>) {
const { users } = props;
const { users, profile } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
const { Theme } = useTheme(user.theme);
const { t } = useLocale();
@@ -84,11 +87,11 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
<h2 className="font-cal font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
<EventTypeDescription className="text-sm" eventType={type} />
</div>
<div className="mt-1">
<div className="mt-1 self-center">
<AvatarGroup
border="border-2 border-white"
truncateAfter={4}
className="flex-shrink-0"
className="flex flex-shrink-0"
size={10}
items={props.users.map((user) => ({
alt: user.name || "",
@@ -102,13 +105,15 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
))}
</ul>
);
const isEmbed = useIsEmbed();
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const query = { ...router.query };
delete query.user; // So it doesn't display in the Link (and make tests fail)
useExposePlanGlobally("PRO");
const nameOrUsername = user.name || user.username || "";
const [evtsToVerify, setEvtsToVerify] = useState<EvtsToVerify>({});
const isEmbed = useIsEmbed();
const telemetry = useTelemetry();
useEffect(() => {
@@ -128,8 +133,17 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
username={isDynamicGroup ? dynamicUsernames.join(", ") : (user.username as string) || ""}
// avatar={user.avatar || undefined}
/>
<div className={"h-screen dark:bg-neutral-900" + isEmbed ? " bg:white m-auto max-w-3xl" : ""}>
<main className="mx-auto max-w-3xl px-4 py-24">
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
<div className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "max-w-3xl" : "")}>
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed
? " border-bookinglightest rounded-md border bg-white dark:bg-neutral-900 sm:dark:border-gray-600"
: "",
"max-w-3xl py-24 px-4"
)}>
{isSingleUser && ( // When we deal with a single user, not dynamic group
<div className="mb-8 text-center">
<AvatarSSR user={user} className="mx-auto mb-4 h-24 w-24" alt={nameOrUsername}></AvatarSSR>
@@ -284,6 +298,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
email: true,
name: true,
bio: true,
brandColor: true,
darkBrandColor: true,
avatar: true,
theme: true,
plan: true,
@@ -298,10 +314,36 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
notFound: true,
};
}
const isDynamicGroup = users.length > 1;
const dynamicNames = isDynamicGroup
? users.map((user) => {
return user.name || "";
})
: [];
const [user] = users; //to be used when dealing with single user, not dynamic group
const profile = isDynamicGroup
? {
name: getGroupName(dynamicNames),
image: null,
theme: null,
weekStart: "Sunday",
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: users.some((user) => {
return !user.allowDynamicBooking;
})
? false
: true,
}
: {
name: user.name || user.username,
image: user.avatar,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
};
const usersIds = users.map((user) => user.id);
const credentials = await prisma.credential.findMany({
where: {
@@ -337,6 +379,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
users,
profile,
user: {
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},

View File

@@ -20,7 +20,22 @@ export type AvailabilityPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type(props: AvailabilityPageProps) {
const { t } = useLocale();
return props.isDynamicGroup && !props.profile.allowDynamicBooking ? (
return props.away ? (
<div className="h-screen dark:bg-neutral-900">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">
😴{" " + t("user_away")}
</h2>
<p className="mx-auto max-w-md">{t("user_away_description")}</p>
</div>
</div>
</div>
</main>
</div>
) : props.isDynamicGroup && !props.profile.allowDynamicBooking ? (
<div className="h-screen dark:bg-neutral-900">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
@@ -118,6 +133,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
darkBrandColor: true,
defaultScheduleId: true,
allowDynamicBooking: true,
away: true,
schedules: {
select: {
availability: true,
@@ -301,6 +317,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
away: user.away,
isDynamicGroup,
profile,
plan: user.plan,

View File

@@ -30,7 +30,22 @@ export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) {
const { t } = useLocale();
return props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
return props.away ? (
<div className="h-screen dark:bg-neutral-900">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="p-8 text-center text-gray-400 dark:text-white">
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">
😴{" " + t("user_away")}
</h2>
<p className="mx-auto max-w-md">{t("user_away_description")}</p>
</div>
</div>
</div>
</main>
</div>
) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
<div className="h-screen dark:bg-neutral-900">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
@@ -71,6 +86,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
away: true,
},
});
@@ -190,12 +206,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
away: user.away,
locationLabels: getLocationLabels(t),
profile,
eventType: eventTypeObject,
booking,
trpcState: ssr.dehydrate(),
isDynamicGroupBooking,
hasHashedBookingLink: false,
hashedLink: null,
},
};
}

View File

@@ -5,7 +5,7 @@ type Props = Record<string, unknown> & DocumentProps;
class MyDocument extends Document<Props> {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
const isEmbed = ctx.req?.url?.includes("embed");
const isEmbed = ctx.req?.url?.includes("embed=");
return { ...initialProps, isEmbed };
}
@@ -27,7 +27,9 @@ class MyDocument extends Document<Props> {
</Head>
{/* Keep the embed hidden till parent initializes and gives it the appropriate styles */}
<body className="bg-gray-100 dark:bg-neutral-900" style={props.isEmbed ? { display: "none" } : {}}>
<body
className={props.isEmbed ? "bg-transparent" : "bg-gray-100 dark:bg-neutral-900"}
style={props.isEmbed ? { display: "none" } : {}}>
<Main />
<NextScript />
</body>

View File

@@ -0,0 +1,34 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
req.session = await getSession({ req });
if (req.method === "GET" && req.session && req.session.user.id && req.query) {
const { "app-credential-type": appCredentialType } = req.query;
if (!appCredentialType && Array.isArray(appCredentialType)) {
return res.status(400);
}
const userId = req.session.user.id;
try {
const installedApp = await prisma?.credential.findFirst({
where: {
type: appCredentialType as string,
userId: userId,
},
});
if (installedApp && !!installedApp.key) {
res.status(200);
} else {
res.status(404);
}
} catch (error) {
res.status(500);
}
} else {
res.status(400);
}
res.end();
}

View File

@@ -1,20 +1,35 @@
import { IdentityProvider } from "@prisma/client";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { IdentityProvider, UserPermissionRole } from "@prisma/client";
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import NextAuth, { Session } from "next-auth";
import { Provider } from "next-auth/providers";
import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import GoogleProvider from "next-auth/providers/google";
import nodemailer, { TransportOptions } from "nodemailer";
import { authenticator } from "otplib";
import path from "path";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
import { serverConfig } from "@calcom/lib/serverConfig";
import ImpersonationProvider from "@ee/lib/impersonation/ImpersonationProvider";
import { ErrorCode, verifyPassword } from "@lib/auth";
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
import prisma from "@lib/prisma";
import { randomString } from "@lib/random";
import { isSAMLLoginEnabled, samlLoginUrl, hostedCal } from "@lib/saml";
import { hostedCal, isSAMLLoginEnabled, samlLoginUrl } from "@lib/saml";
import slugify from "@lib/slugify";
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
const transporter = nodemailer.createTransport<TransportOptions>({
...(serverConfig.transport as TransportOptions),
} as TransportOptions);
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
const providers: Provider[] = [
@@ -90,9 +105,11 @@ const providers: Provider[] = [
username: user.username,
email: user.email,
name: user.name,
role: user.role,
};
},
}),
ImpersonationProvider,
];
if (IS_GOOGLE_LOGIN_ENABLED) {
@@ -141,15 +158,47 @@ if (isSAMLLoginEnabled) {
});
}
if (true) {
const emailsDir = path.resolve(process.cwd(), "lib", "emails", "templates");
providers.push(
EmailProvider({
maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only
// Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify.
sendVerificationRequest: ({ identifier, url }) => {
// Here we add /new endpoint to the callback URL by adding it before &token=.
// This is not elegant but it works. We should probably use a different approach when we can.
url = url.includes("/auth/new") ? url : url.replace("&token", "/auth/new&token");
const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), {
encoding: "utf8",
});
const emailTemplate = Handlebars.compile(emailFile);
transporter.sendMail({
from: `${process.env.EMAIL_FROM}` || "Cal.com",
to: identifier,
subject: "Your sign-in link for Cal.com",
html: emailTemplate({
base_url: WEBSITE_URL,
signin_url: url,
email: identifier,
}),
});
},
})
);
}
const calcomAdapter = CalComAdapter(prisma);
export default NextAuth({
// @ts-ignore
adapter: calcomAdapter,
session: {
strategy: "jwt",
},
secret: process.env.JWT_SECRET,
cookies: defaultCookies(WEBSITE_URL?.startsWith("https://")),
pages: {
signIn: "/auth/login",
signOut: "/auth/logout",
error: "/auth/error", // Error code passed in query string as ?error=
newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest)
},
providers,
callbacks: {
@@ -169,6 +218,8 @@ export default NextAuth({
username: existingUser.username,
name: existingUser.name,
email: existingUser.email,
role: existingUser.role,
impersonatedByUID: token?.impersonatedByUID as number,
};
}
@@ -185,6 +236,8 @@ export default NextAuth({
name: user.name,
username: user.username,
email: user.email,
role: user.role,
impersonatedByUID: user?.impersonatedByUID as number,
};
}
@@ -195,7 +248,6 @@ export default NextAuth({
if (account.provider === "saml") {
idP = IdentityProvider.SAML;
}
const existingUser = await prisma.user.findFirst({
where: {
AND: [
@@ -218,6 +270,8 @@ export default NextAuth({
name: existingUser.name,
username: existingUser.username,
email: existingUser.email,
role: existingUser.role,
impersonatedByUID: token.impersonatedByUID as number,
};
}
@@ -231,11 +285,18 @@ export default NextAuth({
id: token.id as number,
name: token.name,
username: token.username as string,
role: token.role as UserPermissionRole,
impersonatedByUID: token.impersonatedByUID as number,
},
};
return calendsoSession;
},
async signIn({ user, account, profile }) {
async signIn(params) {
const { user, account, profile } = params;
if (account.provider === "email") {
return true;
}
// In this case we've already verified the credentials in the authorize
// callback so we can sign the user in.
if (account.type === "credentials") {
@@ -264,10 +325,20 @@ export default NextAuth({
if (!user.email_verified) {
return "/auth/error?error=unverified-email";
}
// Only google oauth on this path
const provider = account.provider.toUpperCase() as IdentityProvider;
const existingUser = await prisma.user.findFirst({
include: {
accounts: {
where: {
provider: account.provider,
},
},
},
where: {
AND: [{ identityProvider: idP }, { identityProviderId: user.id as string }],
identityProvider: provider,
identityProviderId: account.providerAccountId,
},
});
@@ -275,6 +346,17 @@ export default NextAuth({
// In this case there's an existing user and their email address
// hasn't changed since they last logged in.
if (existingUser.email === user.email) {
try {
// If old user without Account entry we link their google account
if (existingUser.accounts.length === 0) {
const linkAccountWithUserData = { ...account, userId: existingUser.id };
await calcomAdapter.linkAccount(linkAccountWithUserData);
}
} catch (error) {
if (error instanceof Error) {
console.error("Error while linking account of already existing user");
}
}
return true;
}
@@ -335,7 +417,7 @@ export default NextAuth({
return "/auth/error?error=use-identity-login";
}
await prisma.user.create({
const newUser = await prisma.user.create({
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
@@ -347,6 +429,8 @@ export default NextAuth({
identityProviderId: user.id as string,
},
});
const linkAccountNewUserData = { ...account, userId: newUser.id };
await calcomAdapter.linkAccount(linkAccountNewUserData);
return true;
}

View File

@@ -233,6 +233,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const dynamicUserList = Array.isArray(reqBody.user)
? getGroupName(req.body.user)
: getUsernameList(reqBody.user as string);
const hasHashedBookingLink = reqBody.hasHashedBookingLink;
const eventTypeSlug = reqBody.eventTypeSlug;
const eventTypeId = reqBody.eventTypeId;
const tAttendees = await getTranslation(reqBody.language ?? "en", "common");
@@ -673,7 +674,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
await sendRescheduledEmails({ ...evt, additionInformation: metadata });
await sendRescheduledEmails({
...evt,
additionInformation: metadata,
additionalNotes, // Resets back to the addtionalNote input and not the overriden value
});
}
// If it's not a reschedule, doesn't require confirmation and there's no price,
// Create a booking
@@ -703,13 +708,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
metadata.conferenceData = results[0].createdEvent?.conferenceData;
metadata.entryPoints = results[0].createdEvent?.entryPoints;
}
await sendScheduledEmails({ ...evt, additionInformation: metadata });
await sendScheduledEmails({
...evt,
additionInformation: metadata,
additionalNotes,
});
}
}
if (eventType.requiresConfirmation && !rescheduleUid) {
await sendOrganizerRequestEmail(evt);
await sendAttendeeRequestEmail(evt, attendeesList[0]);
await sendOrganizerRequestEmail({ ...evt, additionalNotes });
await sendAttendeeRequestEmail({ ...evt, additionalNotes }, attendeesList[0]);
}
if (typeof eventType.price === "number" && eventType.price > 0 && !originalRescheduledBooking?.paid) {
@@ -772,6 +781,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
},
});
// refresh hashed link if used
const urlSeed = `${users[0].username}:${dayjs(req.body.start).utc().format()}`;
const hashedUid = translator.fromUUID(uuidv5(urlSeed, uuidv5.URL));
if (hasHashedBookingLink) {
await prisma.hashedLink.update({
where: {
link: reqBody.hashedLink as string,
},
data: {
link: hashedUid,
},
});
}
// booking successful
return res.status(201).json(booking);

View File

@@ -174,14 +174,6 @@ const handler = async (
}
});
// Creating cancelled event as placeholders in calendars, remove when virtual calendar handles it
const eventManager = new EventManager({
credentials: userOwner.credentials,
destinationCalendar: userOwner.destinationCalendar,
});
builder.calendarEvent.title = `Cancelled: ${builder.calendarEvent.title}`;
await eventManager.updateAndSetCancelledPlaceholder(builder.calendarEvent, bookingToReschedule);
// Send emails
await sendRequestRescheduleEmail(builder.calendarEvent, {
rescheduleLink: builder.rescheduleLink,

View File

@@ -1,7 +1,5 @@
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import appStore from "@calcom/app-store";
import { getSession } from "@lib/auth";
import { HttpError } from "@lib/core/http/error";
@@ -19,19 +17,15 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const appName = _appName.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
try {
// TODO: Find a way to dynamically import these modules
// const app = (await import(`@calcom/${appName}`)).default;
const app = appStore[appName as keyof typeof appStore];
if (!(app && "api" in app && apiEndpoint in app.api))
throw new HttpError({ statusCode: 404, message: `API handler not found` });
const handler = app.api[apiEndpoint as keyof typeof app.api] as NextApiHandler;
/* Absolute path didn't work */
const handlerMap = (await import("@calcom/app-store/apiHandlers")).default;
const handlers = await handlerMap[appName as keyof typeof handlerMap];
const handler = handlers[apiEndpoint as keyof typeof handlers] as NextApiHandler;
if (typeof handler !== "function")
throw new HttpError({ statusCode: 404, message: `API handler not found` });
const response = await handler(req, res);
console.log("response", response);
return res.status(200);
} catch (error) {

View File

@@ -76,7 +76,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const token: string = randomBytes(32).toString("hex");
await prisma.verificationRequest.create({
await prisma.verificationToken.create({
data: {
identifier: usernameOrEmail,
token,

View File

@@ -1,31 +1,60 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import prisma from "@lib/prisma";
import { defaultAvatarSrc } from "@lib/profile";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// const username = req.url?.substring(1, req.url.lastIndexOf("/"));
const username = req.query.username as string;
const user = await prisma.user.findUnique({
where: {
username: username,
},
select: {
avatar: true,
email: true,
},
});
const teamname = req.query.teamname as string;
let identity;
if (username) {
const user = await prisma.user.findUnique({
where: {
username: username,
},
select: {
avatar: true,
email: true,
},
});
identity = {
name: username,
email: user?.email,
avatar: user?.avatar,
};
} else if (teamname) {
const team = await prisma.team.findUnique({
where: {
slug: teamname,
},
select: {
logo: true,
},
});
identity = {
name: teamname,
shouldDefaultBeNameBased: true,
avatar: team?.logo,
};
}
const emailMd5 = crypto
.createHash("md5")
.update((user?.email as string) || "guest@example.com")
.update((identity?.email as string) || "guest@example.com")
.digest("hex");
const img = user?.avatar;
const img = identity?.avatar;
if (!img) {
let defaultSrc = defaultAvatarSrc({ md5: emailMd5 });
if (identity?.shouldDefaultBeNameBased) {
defaultSrc = getPlaceholderAvatar(null, identity.name);
}
res.writeHead(302, {
Location: defaultAvatarSrc({ md5: emailMd5 }),
Location: defaultSrc,
});
res.end();
} else if (!img.includes("data:image")) {
res.writeHead(302, {

View File

@@ -1,13 +1,14 @@
import fs from "fs";
import matter from "gray-matter";
import { GetStaticPaths, GetStaticPathsResult, GetStaticPropsContext } from "next";
import { GetStaticPaths, GetStaticPropsContext } from "next";
import { MDXRemote } from "next-mdx-remote";
import { serialize } from "next-mdx-remote/serialize";
import Image from "next/image";
import Link from "next/link";
import path from "path";
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
import prisma from "@calcom/prisma";
import useMediaQuery from "@lib/hooks/useMediaQuery";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -68,11 +69,8 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
}
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
const appStore = getAppRegistry();
const paths = appStore.reduce((paths, app) => {
paths.push({ params: { slug: app.slug } });
return paths;
}, [] as GetStaticPathsResult<{ slug: string }>["paths"]);
const appStore = await prisma.app.findMany({ select: { slug: true } });
const paths = appStore.map(({ slug }) => ({ params: { slug } }));
return {
paths,
@@ -81,23 +79,19 @@ export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
};
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
const appStore = getAppRegistry();
if (typeof ctx.params?.slug !== "string") return { notFound: true };
if (typeof ctx.params?.slug !== "string") {
return {
notFound: true,
};
}
const app = await prisma.app.findUnique({
where: { slug: ctx.params.slug },
});
const singleApp = appStore.find((app) => app.slug === ctx.params?.slug);
if (!app) return { notFound: true };
if (!singleApp) {
return {
notFound: true,
};
}
const singleApp = await getAppWithMetadata(app);
const appDirname = singleApp.type.replace("_", "");
if (!singleApp) return { notFound: true };
const appDirname = app.dirName;
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/README.mdx`);
const postFilePath = path.join(README_PATH);
let source = "";

View File

@@ -50,7 +50,7 @@ export default function Apps({ appStore }: InferGetStaticPropsType<typeof getSta
}
export const getStaticPaths = async () => {
const appStore = getAppRegistry();
const appStore = await getAppRegistry();
const paths = appStore.reduce((categories, app) => {
if (!categories.includes(app.category)) {
categories.push(app.category);
@@ -67,7 +67,7 @@ export const getStaticPaths = async () => {
export const getStaticProps = async () => {
return {
props: {
appStore: getAppRegistry(),
appStore: await getAppRegistry(),
},
};
};

View File

@@ -30,7 +30,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
}
export const getStaticProps = async () => {
const appStore = getAppRegistry();
const appStore = await getAppRegistry();
const categories = appStore.reduce((c, app) => {
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
return c;

View File

@@ -24,7 +24,7 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
}
export const getStaticProps = async () => {
const appStore = getAppRegistry();
const appStore = await getAppRegistry();
const categories = appStore.reduce((c, app) => {
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
return c;

View File

@@ -18,8 +18,8 @@ import { trpc } from "@lib/trpc";
import AppsShell from "@components/AppsShell";
import { ClientSuspense } from "@components/ClientSuspense";
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
import Loader from "@components/Loader";
import Shell, { ShellSubHeading } from "@components/Shell";
import SkeletonLoader from "@components/apps/SkeletonLoader";
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
import IntegrationListItem from "@components/integrations/IntegrationListItem";
@@ -256,7 +256,7 @@ function Web3Container() {
return (
<>
<ShellSubHeading title="Web3" subtitle={t("meet_people_with_the_same_tokens")} />
<ShellSubHeading title="Web3" subtitle={t("meet_people_with_the_same_tokens")} className="mt-10" />
<div className="lg:col-span-9 lg:pb-8">
<List>
<ListItem className={classNames("flex-col")}>
@@ -332,9 +332,13 @@ export default function IntegrationsPage() {
const { t } = useLocale();
return (
<Shell heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")} large>
<Shell
heading={t("installed_apps")}
subtitle={t("manage_your_connected_apps")}
large
customLoader={<SkeletonLoader />}>
<AppsShell>
<ClientSuspense fallback={<Loader />}>
<ClientSuspense fallback={<SkeletonLoader />}>
<IntegrationsContainer />
<CalendarListContainer />
<WebhookListContainer title={t("webhooks")} subtitle={t("receive_cal_meeting_data")} />

View File

@@ -7,6 +7,7 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { EmailField, Form, PasswordField } from "@calcom/ui/form/fields";
@@ -61,12 +62,15 @@ export default function Login({
let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "";
// If not absolute URL, make it absolute
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
callbackUrl = `${WEBAPP_URL}/${callbackUrl}`;
}
callbackUrl = getSafeRedirectUrl(callbackUrl);
const LoginFooter = (
<span>
{t("dont_have_an_account")}{" "}

View File

@@ -1,6 +1,7 @@
import { CheckIcon } from "@heroicons/react/outline";
import { GetServerSidePropsContext } from "next";
import { useSession, signOut } from "next-auth/react";
import { getCookieParser } from "next/dist/server/api-utils";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
@@ -51,6 +52,11 @@ export default function Logout(props: Props) {
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
// Deleting old cookie manually, remove this code after all existing cookies have expired
context.res.setHeader(
"Set-Cookie",
"next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"
);
return {
props: {

View File

@@ -0,0 +1,6 @@
export default function NewUserPage() {
if (typeof window !== "undefined") {
window.location.assign(process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com");
}
return null;
}

View File

@@ -153,14 +153,14 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
notFound: true,
};
}
const verificationRequest = await prisma.verificationRequest.findUnique({
const verificationToken = await prisma.verificationToken.findUnique({
where: {
token,
},
});
// for now, disable if no verificationRequestToken given or token expired
if (!verificationRequest || verificationRequest.expires < new Date()) {
// for now, disable if no verificationToken given or token expired
if (!verificationToken || verificationToken.expires < new Date()) {
return {
notFound: true,
};
@@ -170,7 +170,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
where: {
AND: [
{
email: verificationRequest.identifier,
email: verificationToken.identifier,
},
{
emailVerified: {
@@ -194,7 +194,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
props: {
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
isSAMLLoginEnabled,
email: verificationRequest.identifier,
email: verificationToken.identifier,
trpcState: ssr.dehydrate(),
},
};

View File

@@ -16,6 +16,7 @@ import { inferQueryOutput, trpc } from "@lib/trpc";
import EmptyScreen from "@components/EmptyScreen";
import Shell from "@components/Shell";
import { NewScheduleButton } from "@components/availability/NewScheduleButton";
import SkeletonLoader from "@components/availability/SkeletonLoader";
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
const { t, i18n } = useLocale();
@@ -105,8 +106,12 @@ export default function AvailabilityPage() {
const { t } = useLocale();
return (
<div>
<Shell heading={t("availability")} subtitle={t("configure_availability")} CTA={<NewScheduleButton />}>
<WithQuery success={({ data }) => <AvailabilityList {...data} />} />
<Shell
heading={t("availability")}
subtitle={t("configure_availability")}
CTA={<NewScheduleButton />}
customLoader={<SkeletonLoader />}>
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
</Shell>
</div>
);

View File

@@ -12,9 +12,9 @@ import { inferQueryInput, trpc } from "@lib/trpc";
import BookingsShell from "@components/BookingsShell";
import EmptyScreen from "@components/EmptyScreen";
import Loader from "@components/Loader";
import Shell from "@components/Shell";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"];
@@ -45,8 +45,11 @@ export default function Bookings() {
const isEmpty = !query.data?.pages[0]?.bookings.length;
return (
<Shell heading={t("bookings")} subtitle={t("bookings_description")}>
<WipeMyCalActionButton trpc={trpc} />
<Shell
heading={t("bookings")}
subtitle={t("bookings_description")}
customLoader={<SkeletonLoader></SkeletonLoader>}>
<WipeMyCalActionButton trpc={trpc} bookingStatus={status} bookingsEmpty={isEmpty} />
<BookingsShell>
<div className="-mx-4 flex flex-col sm:mx-auto">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
@@ -54,7 +57,7 @@ export default function Bookings() {
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{(query.status === "loading" || query.status === "idle") && <Loader />}
{(query.status === "loading" || query.status === "idle") && <SkeletonLoader />}
{query.status === "success" && !isEmpty && (
<>
<div className="mt-6 overflow-hidden rounded-sm border border-b border-gray-200">
@@ -72,6 +75,7 @@ export default function Bookings() {
</div>
<div className="p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>

View File

@@ -101,8 +101,8 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
className="mb-5 sm:mb-6"
/>
<div className="space-x-2 text-center rtl:space-x-reverse">
<Button color="secondary" onClick={() => router.back()}>
{t("back_to_bookings")}
<Button color="secondary" onClick={() => router.push("/reschedule/" + uid)}>
{t("reschedule_this")}
</Button>
<Button
data-testid="cancel"

View File

@@ -0,0 +1,182 @@
import { Prisma } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getWorkingHours } from "@lib/availability";
import { GetBookingType } from "@lib/getBooking";
import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
import { ssrInit } from "@server/lib/ssr";
export type AvailabilityPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type(props: AvailabilityPageProps) {
return <AvailabilityPage {...props} />;
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const link = asStringOrNull(context.query.link) || "";
const slug = asStringOrNull(context.query.slug) || "";
const dateParam = asStringOrNull(context.query.date);
const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
id: true,
title: true,
availability: true,
description: true,
length: true,
price: true,
currency: true,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
schedulingType: true,
userId: true,
schedule: {
select: {
availability: true,
timeZone: true,
},
},
hidden: true,
slug: true,
minimumBookingNotice: true,
beforeEventBuffer: true,
afterEventBuffer: true,
timeZone: true,
metadata: true,
slotInterval: true,
users: {
select: {
id: true,
avatar: true,
name: true,
username: true,
hideBranding: true,
plan: true,
timeZone: true,
},
},
});
const hashedLink = await prisma.hashedLink.findUnique({
where: {
link,
},
select: {
eventTypeId: true,
eventType: {
select: eventTypeSelect,
},
},
});
const userId = hashedLink?.eventType.userId || hashedLink?.eventType.users[0]?.id;
if (!userId)
return {
notFound: true,
};
if (hashedLink?.eventType.slug !== slug)
return {
notFound: true,
};
const users = await prisma.user.findMany({
where: {
id: userId,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
startTime: true,
endTime: true,
timeZone: true,
weekStart: true,
availability: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
defaultScheduleId: true,
allowDynamicBooking: true,
away: true,
schedules: {
select: {
availability: true,
timeZone: true,
id: true,
},
},
theme: true,
plan: true,
},
});
if (!users || !users.length) {
return {
notFound: true,
};
}
const [user] = users;
const eventTypeObject = Object.assign({}, hashedLink.eventType, {
metadata: {} as JSONObject,
periodStartDate: hashedLink.eventType.periodStartDate?.toString() ?? null,
periodEndDate: hashedLink.eventType.periodEndDate?.toString() ?? null,
slug,
});
const schedule = {
...user.schedules.filter(
(schedule) => !user.defaultScheduleId || schedule.id === user.defaultScheduleId
)[0],
};
const timeZone = schedule.timeZone || user.timeZone;
const workingHours = getWorkingHours(
{
timeZone,
},
schedule.availability || user.availability
);
eventTypeObject.schedule = null;
eventTypeObject.availability = [];
let booking: GetBookingType | null = null;
const profile = {
name: user.name || user.username,
image: user.avatar,
slug: user.username,
theme: user.theme,
weekStart: user.weekStart,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
};
return {
props: {
away: user.away,
isDynamicGroup: false,
profile,
plan: user.plan,
date: dateParam,
eventType: eventTypeObject,
workingHours,
trpcState: ssr.dehydrate(),
previousPage: context.req.headers.referer ?? null,
booking,
},
};
};

View File

@@ -0,0 +1,163 @@
import { Prisma } from "@prisma/client";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { getLocationLabels } from "@calcom/app-store/utils";
import { asStringOrThrow } from "@lib/asStringOrNull";
import prisma from "@lib/prisma";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage";
import { getTranslation } from "@server/lib/i18n";
import { ssrInit } from "@server/lib/ssr";
dayjs.extend(utc);
dayjs.extend(timezone);
export type HashLinkPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: HashLinkPageProps) {
return <BookingPage {...props} />;
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const link = asStringOrThrow(context.query.link as string);
const slug = context.query.slug as string;
const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
id: true,
title: true,
slug: true,
description: true,
length: true,
locations: true,
customInputs: true,
periodType: true,
periodDays: true,
periodStartDate: true,
periodEndDate: true,
metadata: true,
periodCountCalendarDays: true,
price: true,
currency: true,
disableGuests: true,
userId: true,
users: {
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
theme: true,
},
},
});
const hashedLink = await prisma.hashedLink.findUnique({
where: {
link,
},
select: {
eventTypeId: true,
eventType: {
select: eventTypeSelect,
},
},
});
const userId = hashedLink?.eventType.userId || hashedLink?.eventType.users[0]?.id;
if (!userId)
return {
notFound: true,
};
const users = await prisma.user.findMany({
where: {
id: userId,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
theme: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
},
});
if (!users.length) return { notFound: true };
const [user] = users;
const eventTypeRaw = hashedLink?.eventType;
if (!eventTypeRaw) return { notFound: true };
const credentials = await prisma.credential.findMany({
where: {
userId: {
in: users.map((user) => user.id),
},
},
select: {
id: true,
type: true,
key: true,
},
});
const web3Credentials = credentials.find((credential) => credential.type.includes("_web3"));
const eventType = {
...eventTypeRaw,
metadata: (eventTypeRaw.metadata || {}) as JSONObject,
isWeb3Active:
web3Credentials && web3Credentials.key
? (((web3Credentials.key as JSONObject).isWeb3Active || false) as boolean)
: false,
};
const eventTypeObject = [eventType].map((e) => {
return {
...e,
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
};
})[0];
const profile = {
name: user.name || user.username,
image: user.avatar,
slug: user.username,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
eventName: null,
};
const t = await getTranslation(context.locale ?? "en", "common");
return {
props: {
locationLabels: getLocationLabels(t),
profile,
eventType: eventTypeObject,
booking: null,
trpcState: ssr.dehydrate(),
isDynamicGroupBooking: false,
hasHashedBookingLink: true,
hashedLink: link,
},
};
}

View File

@@ -2,6 +2,7 @@ import { GlobeAltIcon, PhoneIcon, XIcon } from "@heroicons/react/outline";
import {
ChevronRightIcon,
ClockIcon,
DocumentDuplicateIcon,
DocumentIcon,
ExternalLinkIcon,
LinkIcon,
@@ -28,6 +29,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
import { JSONObject } from "superjson/dist/types";
import { z } from "zod";
import { SelectGifInput } from "@calcom/app-store/giphy/components";
import getApps, { getLocationOptions, hasIntegration } from "@calcom/app-store/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
@@ -52,6 +54,7 @@ import { ClientSuspense } from "@components/ClientSuspense";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
import Loader from "@components/Loader";
import Shell from "@components/Shell";
import { Tooltip } from "@components/Tooltip";
import { UpgradeToProDialog } from "@components/UpgradeToProDialog";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
@@ -197,7 +200,15 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
prefix: t("indefinitely_into_future"),
},
];
const { eventType, locationOptions, team, teamMembers, hasPaymentIntegration, currency } = props;
const {
eventType,
locationOptions,
team,
teamMembers,
hasPaymentIntegration,
currency,
hasGiphyIntegration,
} = props;
const router = useRouter();
@@ -262,6 +273,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const [requirePayment, setRequirePayment] = useState(eventType.price > 0);
const [advancedSettingsVisible, setAdvancedSettingsVisible] = useState(false);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
useEffect(() => {
const fetchTokens = async () => {
@@ -442,6 +454,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
team ? `team/${team.slug}` : eventType.users[0].username
}/${eventType.slug}`;
const placeholderHashedLink = `${process.env.NEXT_PUBLIC_WEBSITE_URL}/d/${
eventType.hashedLink ? eventType.hashedLink.link : "xxxxxxxxxxxxxxxxx"
}/${eventType.slug}`;
const mapUserToValue = ({
id,
name,
@@ -471,6 +487,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
currency: string;
hidden: boolean;
hideCalendarNotes: boolean;
hashedLink: boolean;
locations: { type: LocationType; address?: string; link?: string }[];
customInputs: EventTypeCustomInput[];
users: string[];
@@ -488,6 +505,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
externalId: string;
};
successRedirectUrl: string;
giphyThankYouPage: string;
}>({
defaultValues: {
locations: eventType.locations || [],
@@ -906,6 +924,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
periodDates,
periodCountCalendarDays,
smartContractAddress,
giphyThankYouPage,
beforeBufferTime,
afterBufferTime,
locations,
@@ -923,11 +942,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
id: eventType.id,
beforeEventBuffer: beforeBufferTime,
afterEventBuffer: afterBufferTime,
metadata: smartContractAddress
? {
smartContractAddress,
}
: "",
metadata: {
...(smartContractAddress ? { smartContractAddress } : {}),
...(giphyThankYouPage ? { giphyThankYouPage } : {}),
},
});
}}
className="space-y-6">
@@ -1117,7 +1135,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
open={advancedSettingsVisible}
onOpenChange={() => setAdvancedSettingsVisible(!advancedSettingsVisible)}>
<>
<CollapsibleTrigger type="button" className="flex w-full">
<CollapsibleTrigger
type="button"
data-testid="show-advanced-settings"
className="flex w-full">
<ChevronRightIcon
className={`${
advancedSettingsVisible ? "rotate-90 transform" : ""
@@ -1127,7 +1148,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
{t("show_advanced_settings")}
</span>
</CollapsibleTrigger>
<CollapsibleContent className="mt-4 space-y-6">
<CollapsibleContent data-testid="advanced-settings-content" className="mt-4 space-y-6">
{/**
* Only display calendar selector if user has connected calendars AND if it's not
* a team event. Since we don't have logic to handle each attende calendar (for now).
@@ -1330,6 +1351,65 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
)}
/>
<Controller
name="hashedLink"
control={formMethods.control}
defaultValue={eventType.hashedLink ? true : false}
render={() => (
<>
<CheckboxField
id="hashedLink"
name="hashedLink"
label={t("hashed_link")}
description={t("hashed_link_description")}
defaultChecked={eventType.hashedLink ? true : false}
onChange={(e) => {
setHashedLinkVisible(e?.target.checked);
formMethods.setValue("hashedLink", e?.target.checked);
}}
/>
{hashedLinkVisible && (
<div className="block items-center sm:flex">
<div className="min-w-48 mb-4 sm:mb-0"></div>
<div className="w-full">
<div className="relative mt-1 flex w-full">
<input
disabled
data-testid="generated-hash-url"
type="text"
className=" grow select-none border-gray-300 bg-gray-50 text-sm text-gray-500 ltr:rounded-l-sm rtl:rounded-r-sm"
defaultValue={placeholderHashedLink}
/>
<Tooltip
content={
eventType.hashedLink
? t("copy_to_clipboard")
: t("enabled_after_update")
}>
<Button
color="minimal"
onClick={() => {
if (eventType.hashedLink) {
navigator.clipboard.writeText(placeholderHashedLink);
showToast("Link copied!", "success");
}
}}
type="button"
className="text-md flex items-center border border-gray-300 px-2 py-1 text-sm font-medium text-gray-700 ltr:rounded-r-sm ltr:border-l-0 rtl:rounded-l-sm rtl:border-r-0">
<DocumentDuplicateIcon className="w-6 p-1 text-neutral-500" />
</Button>
</Tooltip>
</div>
<span className="text-xs text-gray-500">
The URL will regenerate after each use
</span>
</div>
</div>
)}
</>
)}
/>
<hr className="my-2 border-neutral-200" />
<Controller
name="minimumBookingNotice"
@@ -1424,7 +1504,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<div className="inline-flex">
<input
type="number"
className="block w-12 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
className="block w-16 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
placeholder="30"
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
@@ -1655,6 +1735,39 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
</div>
</>
)}
{hasGiphyIntegration && (
<>
<hr className="border-neutral-200" />
<div className="block sm:flex">
<div className="min-w-48 mb-4 sm:mb-0">
<label
htmlFor="gif"
className="mt-2 flex text-sm font-medium text-neutral-700">
{t("confirmation_page_gif")}
</label>
</div>
<div className="flex flex-col">
<div className="w-full">
<div className="block items-center sm:flex">
<div className="w-full">
<div className="relative flex items-start">
<div className="flex items-center">
<SelectGifInput
defaultValue={eventType?.metadata?.giphyThankYouPage as string}
onChange={(url) => {
formMethods.setValue("giphyThankYouPage", url);
}}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</>
)}
</CollapsibleContent>
</>
{/* )} */}
@@ -1663,7 +1776,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<Button href="/event-types" color="secondary" tabIndex={-1}>
{t("cancel")}
</Button>
<Button type="submit" disabled={updateMutation.isLoading}>
<Button type="submit" data-testid="update-eventtype" disabled={updateMutation.isLoading}>
{t("update")}
</Button>
</div>
@@ -1941,6 +2054,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
beforeEventBuffer: true,
afterEventBuffer: true,
slotInterval: true,
hashedLink: true,
successRedirectUrl: true,
team: {
select: {
@@ -1990,6 +2104,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
type: true,
key: true,
userId: true,
appId: true,
},
});
@@ -2005,6 +2120,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
: false,
};
const hasGiphyIntegration = !!credentials.find((credential) => credential.type === "giphy_other");
// backwards compat
if (eventType.users.length === 0 && !eventType.team) {
const fallbackUser = await prisma.user.findUnique({
@@ -2062,6 +2179,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
team: eventTypeObject.team || null,
teamMembers,
hasPaymentIntegration,
hasGiphyIntegration,
currency,
},
};

View File

@@ -18,6 +18,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { Button } from "@calcom/ui";
@@ -41,6 +42,7 @@ import { Tooltip } from "@components/Tooltip";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup";
import Badge from "@components/ui/Badge";
@@ -230,7 +232,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
)}
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
<div className="flex justify-between rtl:space-x-reverse">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.users?.length > 1 && (
<AvatarGroup
border="border-2 border-white"
@@ -267,7 +269,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</Tooltip>
<Dropdown>
<DropdownMenuTrigger
className="h-10 w-10 cursor-pointer rounded-sm border border-transparent text-neutral-500 hover:border-gray-300 hover:text-neutral-900"
className="h-10 w-10 cursor-pointer rounded-sm border border-transparent text-neutral-500 hover:border-gray-300 hover:text-neutral-900 focus:border-gray-300"
data-testid={"event-type-options-" + type.id}>
<DotsHorizontalIcon className="h-5 w-5 group-hover:text-gray-800" />
</DropdownMenuTrigger>
@@ -451,45 +453,48 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
);
};
const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeadingProps): JSX.Element => (
<div className="mb-4 flex">
<Link href="/settings/teams">
<a>
<Avatar
alt={profile?.name || ""}
imageSrc={profile?.image || undefined}
size={8}
className="mt-1 inline ltr:mr-2 rtl:ml-2"
/>
</a>
</Link>
<div>
const EventTypeListHeading = ({ profile, membershipCount }: EventTypeListHeadingProps): JSX.Element => {
console.log(profile.slug);
return (
<div className="mb-4 flex">
<Link href="/settings/teams">
<a className="font-bold">{profile?.name || ""}</a>
<a>
<Avatar
alt={profile?.name || ""}
imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
size={8}
className="mt-1 inline ltr:mr-2 rtl:ml-2"
/>
</a>
</Link>
{membershipCount && (
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2">
<Link href="/settings/teams">
<a>
<Badge variant="gray">
<UsersIcon className="mr-1 -mt-px inline h-3 w-3" />
{membershipCount}
</Badge>
</a>
</Link>
</span>
)}
{profile?.slug && (
<Link href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${profile.slug}`}>
<a className="block text-xs text-neutral-500">{`${process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(
"https://",
""
)}/${profile.slug}`}</a>
<div>
<Link href="/settings/teams">
<a className="font-bold">{profile?.name || ""}</a>
</Link>
)}
{membershipCount && (
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2">
<Link href="/settings/teams">
<a>
<Badge variant="gray">
<UsersIcon className="mr-1 -mt-px inline h-3 w-3" />
{membershipCount}
</Badge>
</a>
</Link>
</span>
)}
{profile?.slug && (
<Link href={`${process.env.NEXT_PUBLIC_WEBSITE_URL}/${profile.slug}`}>
<a className="block text-xs text-neutral-500">{`${process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(
"https://",
""
)}/${profile.slug}`}</a>
</Link>
)}
</div>
</div>
</div>
);
);
};
const CreateFirstEventTypeView = ({ canAddEvents, profiles }: CreateEventTypeProps) => {
const { t } = useLocale();
@@ -523,8 +528,13 @@ const EventTypesPage = () => {
<title>Home | Cal.com</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Shell heading={t("event_types_page_title")} subtitle={t("event_types_page_subtitle")} CTA={<CTA />}>
<Shell
heading={t("event_types_page_title")}
subtitle={t("event_types_page_subtitle")}
CTA={<CTA />}
customLoader={<SkeletonLoader />}>
<WithQuery
customLoader={<SkeletonLoader />}
success={({ data }) => (
<>
{data.viewer.plan === "FREE" && !data.viewer.canAddEvents && (

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