329 Commits
v1.0 ... v1.1

Author SHA1 Message Date
Alex Johansson
f3c95fa3de fix i18n flicker on auth pages (#1183)
* fix flicker on `/auth/login`

* fix flicker on logout

* fix flicker on error

* fix i18n flicker on signup
2021-11-16 18:12:08 +01:00
Lola
669b7798db Enable Recording in Daily Video Calls (#1141)
* ⬆️ Bump tailwindcss from 2.2.14 to 2.2.15

Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 2.2.14 to 2.2.15.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v2.2.14...v2.2.15)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* updating cal will provide a zoom meeting url

* updating cal will provide a zoom meeting url

* modifying how daily emails send

* modifying how daily emails send

* daily table

* migration updates

* rebasing updates

* updating Daily references to a new table

* merge updates, adding Daily references to book/events.ts

* updated video email templates to remove Daily specific references

* updating the events.ts and refactoring in the event manager

* removing the package-lock

* updating some of the internal Daily notes

* removing handle errors raw from the Daily video client

* prettier formatting fixes

* Added the Daily location to calendar events and updated Cal video references to Daily.co video

* updating references to create in event manager to check for Daily video

* adding a daily interface in the event manager

* adding daily to the location labels

* resolving yarn merge conflicts

* removing prettier auto and refactoring integrations: daily in the event manager

* removing changes to estlintrc.json

* updating message when daily video call meeting has not started

* updated modal time buffer

* changed video calls to Cal colors

* removing change to undoing change to lodash/merge on the event manager

* removing change on the event-types to match the main repo

* updating the border color in daily video calls

* updating cal will provide a zoom meeting url

* updating cal will provide a zoom meeting url

* modifying how daily emails send

* modifying how daily emails send

* daily table

* migration updates

* rebasing updates

* updating Daily references to a new table

* merge updates, adding Daily references to book/events.ts

* updated video email templates to remove Daily specific references

* updating the events.ts and refactoring in the event manager

* removing the package-lock

* updating some of the internal Daily notes

* removing handle errors raw from the Daily video client

* prettier formatting fixes

* Added the Daily location to calendar events and updated Cal video references to Daily.co video

* updating references to create in event manager to check for Daily video

* adding a daily interface in the event manager

* adding daily to the location labels

* resolving yarn merge conflicts

* removing prettier auto and refactoring integrations: daily in the event manager

* removing changes to estlintrc.json

* updated modal time buffer

* updates to enable recording

* removed the console log line for debugging int he DailyVideoAdapter

* removed the env copy created here

* updating readme and chaging Daily Scale Plan variable to true

* merge changes

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lola-Ojabowale <lola.ojabowale@gmail.com>
2021-11-16 14:12:10 +00:00
Syed Ali Shahbaz
4e01b13133 Feature/cal 677 brand color in settingsprofile (#1158)
* added CSS variable --brand-color

* added CustomBranding component

* prisma update for brand color

* added brandcolor to user context in viewer.me

* conflict resolution

* added brandColor input and mutation

* custom brand color to availability

* brandColor added to BookingPage

* fixed availability, booking for team and added customBranding to success

* brandColor added to cancel/uid

* requested changes

* lint fix

* further changes

* lint fix
2021-11-16 14:21:46 +05:30
Omar López
11e7779a58 Allows nameless users to Signout and update profile (#1181) 2021-11-15 18:08:04 -07:00
Alex Johansson
7de35dc4e5 set engines version to >=14 (#1178)
* add `16.x` to `engines`

node.16 is LTS now

* Update package.json

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-11-16 00:20:12 +00:00
Alex Johansson
0a34512e3b availability timezone fix (#1180) 2021-11-15 20:32:10 +00:00
Bailey Pumfleet
9fd078c59d Revert "automerge and approve crowdin (#1167)" (#1174) 2021-11-15 18:47:20 +00:00
Alex Johansson
11269229ae add happy path test for booking an event (#1177) 2021-11-15 15:03:04 +00:00
Bailey Pumfleet
6b171a6f87 Manually reorder event types (#1142)
* Add event type reordering

* Add migration for position field

* hack on a hack

* can edit

* fix ordering

* Remove console.log

Co-authored-by: Alex Johansson <alexander@n1s.se>

Co-authored-by: KATT <alexander@n1s.se>
2021-11-15 12:25:49 +00:00
Peer Richelsen
6fa980f801 added cal.crowdin.com/cal to readme 2021-11-13 21:24:22 +00:00
Alex van Andel
fd2adae10f Tweak my-4 to mb-4 (#1164)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-11-13 09:53:01 +00:00
github-actions[bot]
f0ff048f4d New Crowdin translations by Github Action (#1168)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-11-13 09:36:07 +00:00
Alex Johansson
7998b6248d automerge and approve crowdin (#1167)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-11-11 12:05:59 +00:00
github-actions[bot]
6e100a6559 New Crowdin translations by Github Action (#1165)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-11-11 11:32:57 +00:00
Alex Johansson
d4f8030b6b only create event on first calendar integration (#1155)
* only create event on first calendar integration

* fix it

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-11-11 05:50:56 +00:00
Alex van Andel
bf659c0b16 Fixed #1015 - Teams user registration is broken (#1090)
* Fixed #1015 - Teams user registration is broken

* Type fixes for avilability form in onboarding

* Re adds missing strings

* Updates user availability in one query

Tested and working correctly

* Fixes seeder and tests

Co-authored-by: Omar López <zomars@me.com>
2021-11-11 05:44:53 +00:00
Ciarán Hanrahan
16fba702fb typography fixes (#1163) 2021-11-10 23:18:39 +00:00
github-actions[bot]
87b2ecec26 New Crowdin translations by Github Action (#1162)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-11-10 14:24:41 +00:00
github-actions[bot]
990b106c8b New Crowdin translations by Github Action (#1161)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-11-10 14:20:14 +00:00
github-actions[bot]
aaa48372b1 New Crowdin translations by Github Action (#1154)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-11-10 14:02:43 +00:00
Bailey Pumfleet
7c6dd7a73b Change input fields to rounded-sm (#1159) 2021-11-10 11:52:42 +00:00
Alex van Andel
8664d217c9 Feature/availability page revamp (#1032)
* Refactored Schedule component

* Merge branch 'main' into feature/availability-page-revamp

* wip

* Turned value into number, many other TS tweaks

* NodeJS 16x works 100% on my local, but out of scope for this already massive PR

* Fixed TS errors in viewer.tsx and schedule/index.ts

* Reverted next.config.js

* Fixed minor remnant from moving types to @lib/types

* schema comment

* some changes to form handling

* add comments

* Turned ConfigType into number; which seems to be the value preferred by tRPC

* Fixed localized time display during onboarding

* Update components/ui/form/Schedule.tsx

Co-authored-by: Alex Johansson <alexander@n1s.se>

* Added showToast to indicate save success

* Converted number to Date, and also always establish time based on current date

* prevent height flickering of availability

by removing mb-2 of input field

* availabilty: re-added mb-2 but added min-height

* Quite a few bugs discovered, but this seems functional

Co-authored-by: KATT <alexander@n1s.se>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-11-10 11:16:32 +00:00
Mihai C
559ccb8ca7 hotfix: zoom location on emails (#1153)
* fix: zoom location on emails

* test: fix

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-11-09 16:27:33 +00:00
github-actions[bot]
43fa4f6497 New Crowdin translations by Github Action (#1147)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-11-09 15:18:59 +00:00
Peer Richelsen
55a7cc928f removed api docs button from integrations (#1149) 2021-11-09 10:07:02 +00:00
Peer Richelsen
4ce9f9110b fixed darkmode for booking page (#1151) 2021-11-08 23:06:50 +00:00
Peer Richelsen
528c620aa7 fixed dark mode for booking (#1150) 2021-11-08 21:57:32 +00:00
Alex Johansson
df687009bd fix onboarding login glitch (#1118) 2021-11-08 14:10:02 +00:00
Bailey Pumfleet
9befd4abb9 Swap VS Code title bar colours 2021-11-08 13:12:58 +00:00
Syed Ali Shahbaz
a404ca847c updated event title message (#1132)
* updated event title message

* 4 arguments replaced by an object

* translations

* requested changes

* further requested changes

* test fix and other minor changes

* lint fix
2021-11-08 16:34:12 +05:30
Peer Richelsen
0ef6d8b452 added key_strings for /integrations (#1144) 2021-11-07 15:52:48 +00:00
Peer Richelsen
b49553c960 added who string (#1143) 2021-11-06 19:28:30 +00:00
github-actions[bot]
86cb961fa8 New Crowdin translations by Github Action (#1140)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-11-06 12:16:28 +00:00
github-actions[bot]
0e5a7cdd55 New Crowdin translations by Github Action (#1139)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-11-05 20:36:18 +00:00
Alex Johansson
823abfb3da Delete dependabot.yml (#1137)
replacing dependabot with [depfu](depfu.com)
2021-11-05 16:52:35 +00:00
Mihai C
debef8119e Rejected bookings should be displayed in cancelled bookings tab (#1100)
* fix: rejected bookings should be displayed in cancelled bookings tab

* fix: add migration to update status of rejected bookings

* unrelated fix

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2021-11-04 22:24:15 +00:00
Peer Richelsen
773e9ac57e replaced black color with new brand variable to make styling easier (#1125) 2021-11-04 14:30:37 +00:00
dependabot[bot]
0e9c50a58d ⬆️ Bump tailwindcss from 2.2.16 to 2.2.19 (#1128)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 2.2.16 to 2.2.19.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v2.2.16...v2.2.19)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-11-04 14:00:42 +00:00
Peer Richelsen
401016772e added missing roboto weights (#1130) 2021-11-04 13:56:02 +00:00
Peer Richelsen
3c5a84b7b4 removed lexend, upgraded cal sans, added roboto from local instead of google fonts (#1121) 2021-11-04 00:25:47 +00:00
Syed Ali Shahbaz
ac3569b78e removed duplicate unconfirmed status on mobile view (#1119) 2021-11-03 20:37:37 +05:30
Alex Johansson
0ee523643d upgrade to next.js 12 (#1111) 2021-11-03 14:02:17 +00:00
dependabot[bot]
b82f2be391 ⬆️ Bump @typescript-eslint/parser from 4.32.0 to 4.33.0 (#1117)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.32.0 to 4.33.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.33.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-03 13:25:40 +00:00
dependabot[bot]
ae09e220d0 ⬆️ Bump @types/lodash from 4.14.175 to 4.14.176 (#1114)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.175 to 4.14.176.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-03 13:17:04 +00:00
dependabot[bot]
aafbe626fc ⬆️ Bump @hookform/resolvers from 2.8.2 to 2.8.3 (#1104)
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 2.8.2 to 2.8.3.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v2.8.2...v2.8.3)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-03 10:58:15 +00:00
Alex Johansson
a002b194da Fix onboarding OAuth callback glitch (#1079) 2021-11-03 10:47:52 +00:00
dependabot[bot]
d147772d91 ⬆️ Bump @radix-ui/react-switch from 0.1.0 to 0.1.1 (#1106)
Bumps [@radix-ui/react-switch](https://github.com/radix-ui/primitives) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/radix-ui/primitives/releases)
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-switch"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-02 17:30:33 +00:00
dependabot[bot]
8e669cf21e ⬆️ Bump @heroicons/react from 1.0.4 to 1.0.5 (#1107)
Bumps [@heroicons/react](https://github.com/tailwindlabs/heroicons) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/tailwindlabs/heroicons/releases)
- [Commits](https://github.com/tailwindlabs/heroicons/commits)

---
updated-dependencies:
- dependency-name: "@heroicons/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-02 17:24:12 +00:00
dependabot[bot]
1a361283ed ⬆️ Bump @trpc/* from 9.10.1 to 9.12.0 (#1105)
Co-authored-by: KATT <alexander@n1s.se>
2021-11-02 17:18:22 +00:00
Alex Johansson
d4b70162e8 fix locale being empty string (#1112) 2021-11-02 17:09:22 +00:00
Alex Johansson
6318972aa1 temporarily rollback to next.js 11 (#1113) 2021-11-02 18:59:36 +02:00
Alex van Andel
256305eb6b Changed 'jp' to 'ja' (#1110) 2021-11-02 16:15:57 +00:00
Alex Johansson
265c634db9 upgrade to Next.js 12 (#1109) 2021-11-02 14:19:40 +00:00
Alex Johansson
5a25e6daee ignore .js files in type checker (#1108)
makes stuff like #1085 not fail
2021-11-02 16:04:25 +02:00
Alex Johansson
68e062c100 Revert "⬆️ Bump @radix-ui/react-dialog from 0.1.0 to 0.1.1 (#1097)" (#1099)
This reverts commit 15e3b28a23.
2021-11-01 14:43:44 +00:00
dependabot[bot]
15e3b28a23 ⬆️ Bump @radix-ui/react-dialog from 0.1.0 to 0.1.1 (#1097)
Bumps [@radix-ui/react-dialog](https://github.com/radix-ui/primitives) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/radix-ui/primitives/releases)
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dialog"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:27:28 +00:00
dependabot[bot]
d7be47ed10 ⬆️ Bump @types/async from 3.2.8 to 3.2.9 (#1095)
Bumps [@types/async](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/async) from 3.2.8 to 3.2.9.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/async)

---
updated-dependencies:
- dependency-name: "@types/async"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:13:05 +00:00
dependabot[bot]
c1fb56b4b7 ⬆️ Bump superjson from 1.7.5 to 1.8.0 (#1096)
Bumps [superjson](https://github.com/blitz-js/superjson) from 1.7.5 to 1.8.0.
- [Release notes](https://github.com/blitz-js/superjson/releases)
- [Commits](https://github.com/blitz-js/superjson/compare/v1.7.5...v1.8.0)

---
updated-dependencies:
- dependency-name: superjson
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:04:21 +00:00
dependabot[bot]
8a87cb4f3b ⬆️ Bump eslint-plugin-react from 7.26.0 to 7.26.1 (#1098)
Bumps [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react) from 7.26.0 to 7.26.1.
- [Release notes](https://github.com/yannickcr/eslint-plugin-react/releases)
- [Changelog](https://github.com/yannickcr/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yannickcr/eslint-plugin-react/compare/v7.26.0...v7.26.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-react
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:58:20 +00:00
dependabot[bot]
6464cee5a1 ⬆️ Bump react-query from 3.24.4 to 3.30.0 (#1084)
Bumps [react-query](https://github.com/tannerlinsley/react-query) from 3.24.4 to 3.30.0.
- [Release notes](https://github.com/tannerlinsley/react-query/releases)
- [Commits](https://github.com/tannerlinsley/react-query/compare/v3.24.4...v3.30.0)

---
updated-dependencies:
- dependency-name: react-query
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 10:28:57 +00:00
github-actions[bot]
8cb42c44b7 New Crowdin translations by Github Action (#1094)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-11-01 10:00:40 +00:00
Syed Ali Shahbaz
4f75b94d88 mobile view improvement added (#1093)
* mobile view improvement added

* improved max-w
2021-11-01 14:25:43 +05:30
Peer Richelsen
ad46fc121d added jp (japanese) (#1085) 2021-10-31 21:31:21 +00:00
github-actions[bot]
863fc2e5cc New Crowdin translations by Github Action (#1088)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-10-31 19:12:59 +00:00
Alex van Andel
b7435b5b93 Fixed orphaning team event types (#1086) 2021-10-31 10:41:42 +00:00
Peer Richelsen
307856f8e6 added more weight to lexend fallback (#1087) 2021-10-31 01:09:27 +01:00
dependabot[bot]
6635363521 ⬆️ Bump @types/node from 16.10.2 to 16.11.5 (#1046)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 16.10.2 to 16.11.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alex Johansson <alexander@n1s.se>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-30 20:48:05 +00:00
dependabot[bot]
88f10e0586 ⬆️ Bump @hookform/resolvers from 2.8.1 to 2.8.2 (#1083) 2021-10-30 20:58:58 +01:00
dependabot[bot]
9475c5836d ⬆️ Bump playwright from 1.15.2 to 1.16.2 (#1081)
Bumps [playwright](https://github.com/Microsoft/playwright) from 1.15.2 to 1.16.2.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.15.2...v1.16.2)

---
updated-dependencies:
- dependency-name: playwright
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-30 18:10:00 +00:00
dependabot[bot]
a83717363f ⬆️ Bump @daily-co/daily-js from 0.16.0 to 0.21.0 (#1076) 2021-10-30 16:28:46 +00:00
github-actions[bot]
317e3d2ade New Crowdin translations by Github Action (#1074)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-10-30 17:23:38 +01:00
Alex Johansson
a0c2e57891 suspense follow-up fixes (#1080) 2021-10-30 17:03:49 +01:00
Alex Johansson
1790aeb577 refactor /integrations with <Suspense /> (#1078)
* suspense

* iframe embeds

* calendar list container

* rename things as a container

* use list container on onboarding

* fix

* rm code

* newer alpha

* make it work in react 17

* fix

* fix

* make components handle error state through `QueryCell`

* fix constant

* fix type error

* type error

* type fixes

* fix package.lock

* fix webhook invalidate

* fix mt

* fix typo

* pr comment
2021-10-30 15:54:21 +00:00
Syed Ali Shahbaz
78523f7a57 margin fix (#1077) 2021-10-29 21:27:35 +05:30
Omar López
cc25a772a1 Adds missing webhook user relationship (#1070) 2021-10-29 08:13:51 -06:00
Syed Ali Shahbaz
5291dade42 fixed small screen breaking (#1075)
* fixed small screen breaking

* fixed lint err
2021-10-29 18:29:23 +05:30
Syed Ali Shahbaz
ed4587b3af reduces breakpoint max-w (#1072) 2021-10-29 15:33:37 +05:30
Alex van Andel
eefb829f75 Fixes #1021 - correctly replace integrations:google:meet (#1071) 2021-10-29 08:21:32 +00:00
github-actions[bot]
2feed85a1a New Crowdin translations by Github Action (#1048)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-10-29 09:16:36 +01:00
Alex van Andel
a5eb3fce28 Allow confirming COLLECTIVE types (#1069) 2021-10-29 01:50:52 +01:00
Alex van Andel
91fca7477d Fixed opt-in layout plus some tweaks (#1067) 2021-10-28 23:06:37 +00:00
Mihai C
98829d23d3 fix: type errors and translate ee pages (#1050)
* fix: type errors and translate ee pages

* fix: translation key for composed string

* type fixes

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-10-28 22:58:26 +00:00
Alex Johansson
dddb494071 rewrite webhooks to trpc (#1065) 2021-10-28 22:52:39 +00:00
Alex van Andel
41382caa6c Fixed invalid types of i18n locales (string[]) (#1068)
* Fixed invalid types of i18n locales (string[])

* Remaining typefixes done
2021-10-28 22:45:58 +00:00
Omar López
265b76083a Fixes integrations api endpoint (#1066) 2021-10-28 22:30:42 +01:00
Alex van Andel
94f3ae1c64 Added Team with 'pro' and 'free' as members (#1063)
* Added Team with 'pro' and 'free' as members

* Teams fundamentally changes event-type behaviour and requires seperate users
2021-10-28 20:36:45 +01:00
Alex Johansson
5af159cf4e fix loading flicker on /bookings (#1062) 2021-10-28 16:12:30 +00:00
Mihai C
f91de82daf feat: add infinite scroll on bookings tabs (#1059)
* feat: add infinite scroll on bookings tabs

* bookings page infinite scroll PR comments (#1060)

* check if `InteractionObserver` is supported

* revert query cell and use bespoke behaviour

* Update pages/bookings/[status].tsx

Co-authored-by: Mihai C <34626017+mihaic195@users.noreply.github.com>

* load more button

* make inview as a callback

Co-authored-by: Mihai C <34626017+mihaic195@users.noreply.github.com>

* mt-6

* fix: translation strings and remove unnecessary stuff

Co-authored-by: Alex Johansson <alexander@n1s.se>
2021-10-28 15:02:22 +00:00
Omar López
e38086b8fe Refactors video integrations (#1037)
* Fixes error types

* Type fixes

* Refactors video meeting handling

* More type fixes

* Type fixes

* More fixes

* Makes language non optional

* Adds missing translations

* Apply suggestions from code review

Co-authored-by: Alex Johansson <alexander@n1s.se>

* Feedback

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Alex Johansson <alexander@n1s.se>
2021-10-26 10:17:24 -06:00
Bailey Pumfleet
eabb096e14 Ensure you can only delete your own credentials 2021-10-26 16:21:08 +01:00
Alex van Andel
605586ea1b Next turned on allowJs again.. (#1049) 2021-10-26 17:33:35 +03:00
github-actions[bot]
b6b307605b New Crowdin translations by Github Action (#986)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-10-26 14:44:34 +01:00
Lola
9879f9910a Add Modals for Daily Video Calls and updates to Video Call colors (#1027)
* ⬆️ Bump tailwindcss from 2.2.14 to 2.2.15

Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 2.2.14 to 2.2.15.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v2.2.14...v2.2.15)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* updating cal will provide a zoom meeting url

* updating cal will provide a zoom meeting url

* modifying how daily emails send

* modifying how daily emails send

* daily table

* migration updates

* rebasing updates

* updating Daily references to a new table

* merge updates, adding Daily references to book/events.ts

* updated video email templates to remove Daily specific references

* updating the events.ts and refactoring in the event manager

* removing the package-lock

* updating some of the internal Daily notes

* removing handle errors raw from the Daily video client

* prettier formatting fixes

* Added the Daily location to calendar events and updated Cal video references to Daily.co video

* updating references to create in event manager to check for Daily video

* adding a daily interface in the event manager

* adding daily to the location labels

* resolving yarn merge conflicts

* removing prettier auto and refactoring integrations: daily in the event manager

* removing changes to estlintrc.json

* updating message when daily video call meeting has not started

* updated modal time buffer

* changed video calls to Cal colors

* removing change to undoing change to lodash/merge on the event manager

* removing change on the event-types to match the main repo

* updating the border color in daily video calls

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lola-Ojabowale <lola.ojabowale@gmail.com>
2021-10-26 14:10:55 +01:00
dependabot[bot]
22682aa54d ⬆️ Bump ts-jest from 27.0.5 to 27.0.7 (#1040)
Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 27.0.5 to 27.0.7.
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v27.0.5...v27.0.7)

---
updated-dependencies:
- dependency-name: ts-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-25 19:09:43 +00:00
Syed Ali Shahbaz
baba307a9f Feature/cal 605 add webhook test check during webhook (#1035)
* starting point

* lint fix

* add mock placeholder

* simplified a bit

* add some placeholder ui

* err handling

* multiple fixes

* post rebase fixes

* removed extra webhook enabled button

* finishing touches

* added translations

* removed debug remnants

* requested changes

Co-authored-by: KATT <alexander@n1s.se>
2021-10-25 21:45:52 +05:30
Alex Johansson
9842aaaf6a cron fix again - missing \ (#1045) 2021-10-25 16:05:36 +00:00
Alex Johansson
9efa429294 fix cron jobs env/secrets (#1043) 2021-10-25 15:42:16 +00:00
Alex Johansson
a9df3b9ad0 Move cron jobs into GitHub actions (#1006) 2021-10-25 18:16:42 +03:00
Mihai C
8d6fec79d3 feat: add translations for emails and type error fixes overall (#994)
* feat: add translations for forgot password email and misc

* fix: type fixes

* feat: translate invitation email

* fix: e2e tests

* fix: lint

* feat: type fixes and i18n for emails

* Merge main

* fix: jest import on server path

* Merge

* fix: playwright tests

* fix: lint

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-25 14:05:21 +01:00
Mihai C
356d470e16 fix: lowercase email on signup (#1039) 2021-10-25 09:29:54 +00:00
Mihai C
1043b31cc7 fix: lowercase email when loggin in (#1038) 2021-10-25 14:25:28 +05:30
Peer Richelsen
69a54d10df added docker team link 2021-10-23 14:33:49 +01:00
Alex van Andel
1649d41dd5 Adds useUnknownInCatchVariables and disallows .js in tsconfig (#1033) 2021-10-22 21:08:04 +01:00
dependabot[bot]
d780e39241 ⬆️ Bump typescript from 4.4.3 to 4.4.4 (#1025)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.4.3 to 4.4.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.4.3...v4.4.4)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-22 02:35:47 +02:00
dependabot[bot]
3eaf48fac2 ⬆️ Bump @radix-ui/react-dropdown-menu from 0.1.0 to 0.1.1 (#1022)
Bumps [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/radix-ui/primitives/releases)
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-21 16:34:47 +00:00
dependabot[bot]
3f1066e1c3 ⬆️ Bump @trpc/* from 9.9.1 to 9.10.1 (#1023)
Co-authored-by: KATT <alexander@n1s.se>
2021-10-21 16:26:49 +00:00
dependabot[bot]
c42dce2fdb ⬆️ Bump postcss from 8.3.8 to 8.3.11 (#1024)
Bumps [postcss](https://github.com/postcss/postcss) from 8.3.8 to 8.3.11.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.3.8...8.3.11)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-21 18:21:01 +02:00
Peer Richelsen
d6a5d1f3da fixed layout for non-english languages (#1004) 2021-10-20 22:50:32 +00:00
Syed Ali Shahbaz
a6eed6ffcc Enhancement/webhooks redesign (#1005) 2021-10-20 18:23:15 +00:00
Alex Johansson
c28d800aa9 fix i18n flicker on booking pages (#1013) 2021-10-20 16:00:11 +00:00
Alex Johansson
b8e8319b23 await on open change (#1016)
https://github.com/calendso/calendso/pull/988#discussion_r732908578
2021-10-20 15:54:52 +00:00
dependabot[bot]
d63a180bb2 ⬆️ Bump react-timezone-select from 1.1.11 to 1.1.12 (#1010)
Bumps [react-timezone-select](https://github.com/ndom91/react-timezone-select) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/ndom91/react-timezone-select/releases)
- [Commits](https://github.com/ndom91/react-timezone-select/commits)

---
updated-dependencies:
- dependency-name: react-timezone-select
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-20 15:48:16 +00:00
Omar López
85d7122e43 Fixes Apple Calendar onboarding and type fixes (#988)
* Type fixes

* Type fixes

* Attemp to prevent unknown error in prod

* Type fixes

* Type fixes for onboarding

* Extracts ConnectIntegration

* Extracts IntegrationListItem

* Extracts CalendarsList

* Uses CalendarList on onboarding

* Removes deprecated Alert

* Extracts DisconnectIntegration

* Extracts CalendarSwitch

* Extracts ConnectedCalendarsList

* Extracted connectedCalendar logic for reuse

* Extracted SubHeadingTitleWithConnections

* Type fixes

* Fetched connected calendars in onboarding

* Refreshes data on when adding/removing calendars on onboarding

* Removed testing code

* Type fixes

* Feedback

* Moved integration helpers

* I was sleepy

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-10-20 15:42:40 +00:00
dependabot[bot]
bd99a06765 ⬆️ Bump @radix-ui/react-slider from 0.1.0 to 0.1.1 (#1009)
Bumps [@radix-ui/react-slider](https://github.com/radix-ui/primitives) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/radix-ui/primitives/releases)
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-slider"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-20 15:37:02 +00:00
Alex Johansson
9e16007c05 add e2e test retries (#1011) 2021-10-20 15:18:50 +01:00
dependabot[bot]
02c62c18ef ⬆️ Bump @typescript-eslint/eslint-plugin from 4.32.0 to 4.33.0 (#1008)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.32.0 to 4.33.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.33.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-20 15:33:08 +02:00
dependabot[bot]
40377215f6 ⬆️ Bump react-hook-form from 7.17.4 to 7.17.5 (#1001)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.17.4 to 7.17.5.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.17.4...v7.17.5)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-20 10:52:20 +00:00
dependabot[bot]
45ecf0c49c ⬆️ Bump babel-plugin-istanbul from 6.0.0 to 6.1.1 (#1002)
Bumps [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul) from 6.0.0 to 6.1.1.
- [Release notes](https://github.com/istanbuljs/babel-plugin-istanbul/releases)
- [Changelog](https://github.com/istanbuljs/babel-plugin-istanbul/blob/master/CHANGELOG.md)
- [Commits](https://github.com/istanbuljs/babel-plugin-istanbul/compare/v6.0.0...v6.1.1)

---
updated-dependencies:
- dependency-name: babel-plugin-istanbul
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-20 10:47:00 +00:00
Anton Podviaznikov
ade88700fc load @next/bundle-analyzer conditionally if env was set (#999) 2021-10-20 09:08:58 +00:00
Alex Johansson
362e8114a0 Update dependabot.yml (#998)
Allow us to get a few more dependabot options to merge
2021-10-20 09:59:22 +01:00
dependabot[bot]
a9b6b5f066 ⬆️ Bump @tailwindcss/forms from 0.3.3 to 0.3.4 (#997)
Bumps [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) from 0.3.3 to 0.3.4.
- [Release notes](https://github.com/tailwindlabs/tailwindcss-forms/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss-forms/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss-forms/compare/v0.3.3...v0.3.4)

---
updated-dependencies:
- dependency-name: "@tailwindcss/forms"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-20 00:11:35 +00:00
Omar López
06822a2c57 Prevents production build error with bundle analyzer (#996) 2021-10-19 17:51:59 -06:00
Ciarán Hanrahan
1447251c83 changes to bookings and sidebar dropdown (#991) 2021-10-19 12:38:05 +00:00
Bailey Pumfleet
687af03cc3 Improve iframe embed UI (#990) 2021-10-19 11:35:52 +01:00
Alex Johansson
9e69029943 add e2e testing on webhooks and booking happy-path (#936) 2021-10-18 22:07:06 +01:00
Bailey Pumfleet
86d292838c Fix typo in Google font URL 2021-10-18 14:17:48 +01:00
github-actions[bot]
b753d9e5e3 New Crowdin translations by Github Action (#971)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Mihai C <34626017+mihaic195@users.noreply.github.com>
2021-10-18 09:11:41 +00:00
Alex Johansson
d8dac426eb refactor webhooks UI (#982) 2021-10-18 08:02:25 +01:00
Alex van Andel
12f6065d84 Added @types/react-phone-number-input (#983) 2021-10-17 22:10:41 +01:00
Peer Richelsen
415d5fe8bd fix type error in <Shell> Userdropdown (#980)
* removed settings from bottom nav

* truncate bottom nav

* fixed type error in Shell
2021-10-17 13:13:24 +01:00
Peer Richelsen
4d5b5663c0 removing settings from bottom nav for all languages, truncate length (#979)
* removed settings from bottom nav

* truncate bottom nav
2021-10-17 11:36:25 +00:00
Peer Richelsen
656d58b94f moved embed and webhooks from settings into /integrations (#978) 2021-10-17 10:35:25 +01:00
Alex Johansson
c146231b31 speed up e2e tests by reusing cookies (#976) 2021-10-16 17:11:37 -07:00
Alex Johansson
f08a2271fe fix: check-changed-files-job always failing on main (#969) 2021-10-16 09:35:53 -06:00
Alex van Andel
22c4d29db5 Chore: Resolves TS errors related to FileReader/ImageUploader (#965)
* Resolves TS errors related to FileReader/ImageUploader

* Also tackle cropImage errors

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-16 10:42:28 +00:00
Alex Johansson
a67813ee77 if you touch it you fix it (#967)
* wip

* add another

* check

* add ci job

* fix ci

* fix

* maybe

* maybs

* attempt

* test

* maybe

* another attempt

* try v2

* align with normal build

* this should break build

* Revert "this should break build"

This reverts commit 1ba44d18a1d8737aa6232db2d9b9081e892e6ca2.

* tweaks

* prevent breaking on main

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-10-16 10:57:21 +01:00
Justin Mitchell
a32e002fd7 Removes Google calendar scope (#871)
Removes https://www.googleapis.com/auth/calendar scope from Google Calendar as Google will not approve the integration with it requested. Cal.com does not currently utilize this permission either as shown in /pages/api/integrations/googlecalendar/add.ts

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2021-10-15 23:17:56 +01:00
Omar López
3641d5e46e [CAL-493] Implements tRCP on event types (#923)
* Removes unused component

* Refactors useLocale

We don't need to pass the locale prop everywhere

* Event type fixes

* Extracts CreateNewEventDialog

* Implements tRCP for event types
2021-10-15 20:07:00 +01:00
Alex van Andel
c01004b470 Chore: Remove whereAndSelect and fix typescript of main PrismaClient adapter (#966) 2021-10-15 19:31:57 +01:00
github-actions[bot]
78182db99c New Crowdin translations by Github Action (#964)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-10-15 16:24:04 +01:00
Anton Podviaznikov
5ffee8646e use style property for the button in the dialog instead of adding css class manually (#938)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-15 15:21:22 +00:00
Alex van Andel
ce8e9c126b I18n's the i18n language dropdown & weekday using Intl (#955)
* I18n's the i18n language dropdown & weekday using Intl

* Some type fixes

* Trigger locale changes instantly (#958)

* Trigger locale changes instantly

* Restored types

* Capitalize languages across the board

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-10-15 12:32:09 +01:00
github-actions[bot]
b5e176a87e New Crowdin translations by Github Action (#959)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-10-15 11:56:11 +01:00
Mihai C
c4e2b6b458 more strings extractions (#963) 2021-10-15 10:53:42 +00:00
Ciarán Hanrahan
e1f4ba06d8 ui updates to integrations page (#962) 2021-10-15 12:01:49 +02:00
Peer Richelsen
12f72e0283 added meta tags to daily call (#961) 2021-10-15 00:08:14 +01:00
Alex Johansson
59e25ad04e remove unnecessary fetches of api (#960)
* skip repeating fetch of user on session changes
* fix some issues on integrations
2021-10-14 16:00:50 -06:00
Omar López
c2c37b701e Zomars/fixes trpc typo (#957) 2021-10-14 19:22:01 +00:00
Alex Johansson
2ce2bb1ca8 Show loading spinner on <Shell /> until i18n is loaded (#946)
* show loading spinner if i18n isn't loaded

* loading spinner tweaks

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2021-10-14 19:10:44 +00:00
Mihai C
b74dbdca9f fix: missing static files (#954)
* fix: missing static files

* fix: rename file
2021-10-14 14:44:00 +00:00
github-actions[bot]
60db5823c7 New Crowdin translations by Github Action (#951)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-10-14 14:35:36 +00:00
Mihai C
f955ccdef9 i18n: continued (#949)
* more extractions

* fix: failing build
2021-10-14 14:24:21 +00:00
Peer Richelsen
55d77993af added korean to i18n config (#953) 2021-10-14 14:10:53 +00:00
Peer Richelsen
f1eae5fe77 added translations for navigation (#952) 2021-10-14 13:58:17 +00:00
Mihai C
a711670a70 fix prisma error on event-type update (#950)
* fix prisma error on event-type update

* fix hidden event-type update
2021-10-14 14:40:24 +01:00
github-actions[bot]
f4d2f3b3d1 New Crowdin translations by Github Action (#948)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-10-14 14:02:30 +01:00
github-actions[bot]
109cc51e7a New Crowdin translations by Github Action (#947)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2021-10-14 13:55:34 +01:00
Mihai C
1783dae121 add es-419 (#944) 2021-10-14 12:26:11 +00:00
Peer Richelsen
3b844583c9 added lexend as fallback and new version of cal sans (#850) 2021-10-14 11:21:16 +00:00
github-actions[bot]
a71d97a4ad New Crowdin translations by Github Action (#910) 2021-10-14 11:11:47 +00:00
Omar López
0861d7cc61 Ends the war between tRPC and next-i18next (#939)
* Ends the war between tRPC and next-i18next

* Locale fixes

* Linting

* Linting

* trpc i18n (not working) (#942)

* simplify i18n handler and remove redundant(?) fn check

* split up viewer to a "logged in only" and "public"

* wip -- skip first render

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

* Linting

* I18n fixes

* We don't need serverSideTranslations in every page anymore

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Alex Johansson <alexander@n1s.se>
2021-10-14 13:57:49 +03:00
Omar López
26f20e2397 Revert "use base url (incl. app.cal.com) for iframes to support safari (#940)" (#941)
This reverts commit e615347790.
2021-10-14 00:33:43 +01:00
Peer Richelsen
e615347790 use base url (incl. app.cal.com) for iframes to support safari (#940) 2021-10-13 23:10:19 +01:00
Alex van Andel
cf92deb145 Removes Headless UI from the sidenav (#935)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-13 14:00:40 +00:00
Alex Johansson
dc4331ed17 rewrite existing cypress tests to playwright (#931) 2021-10-13 14:26:56 +01:00
Anton Podviaznikov
5c4f4bfd90 use toast instea modal for info messages (#932)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-10-13 13:13:00 +01:00
Alex Johansson
ec6b897191 integration page follow ups (#912)
### Internals

- Replace `lodash.*` packages with plain `lodash` & replace `lodash.*` imports with `lodash/` - should have no impact on bundle size and opens up for us to use all of lodash
- Update `viewer.me` to cherry-pick what we actually need on that query to avoid leaking extra context info
- Update `getIntegrations` to never include `.key`-property to avoid leaking 

### Visual

- Update calendars so `primary` is displayed last
- Update connected calendars so they are in ascending order in which you connected them
2021-10-13 13:35:25 +02:00
Mihai C
9e2f8de313 chore: i18n/extract strings (#934) 2021-10-13 11:49:15 +01:00
Mihai C
bee41b242b chore: extract more strings (#933) 2021-10-13 10:34:55 +01:00
Alex van Andel
4ce4b141b4 Fixes Google Meet showing up during Daily or Zoom events (#930) 2021-10-13 10:13:46 +01:00
Anton Podviaznikov
f9e315d10a renamed "paid plan" to "pro" (#921) 2021-10-12 19:51:46 +00:00
Anton Podviaznikov
0871f5edf8 Refactor two modals (#861)
* refactor one modal into dialog. save profile
2021-10-12 20:43:22 +01:00
Omar López
4b05c56a0d Fixes custom input overflow (#920)
* Fixes custom input overflow

* Truncates also placeholder

* Adds title attributes in booking notes and event descriptions
2021-10-12 18:59:52 +00:00
Omar López
64a01d33ba Fixes custom input dialog (#916) 2021-10-12 19:54:37 +01:00
Omar López
9539d26ac7 Fixes location in payment page (#918)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-10-12 14:54:42 +00:00
Alex Johansson
c94231f777 fix db-up / db-nuke command (#917)
`db-nuke` wasn't properly deleting the docker container
2021-10-12 14:21:57 +00:00
Omar López
cfd70172f0 Refactors useLocale (#908)
* Removes unused component

* Refactors useLocale

We don't need to pass the locale prop everywhere

* Fixes syntax error

* Adds warning for missing localeProps

* Simplify i18n utils

* Update components/I18nLanguageHandler.tsx

Co-authored-by: Mihai C <34626017+mihaic195@users.noreply.github.com>

* Type fixes

Co-authored-by: Mihai C <34626017+mihaic195@users.noreply.github.com>
2021-10-12 13:11:33 +00:00
Peer Richelsen
7dd6fdde7a full width list idems on mobile (#915) 2021-10-12 13:50:43 +01:00
Peer Richelsen
6f204ca521 more layout changes to /integrations (#914)
* added truncate to list
2021-10-12 12:06:12 +00:00
Peer Richelsen
392c8e8da4 minor layout changes to /integrations (#913) 2021-10-12 11:39:02 +00:00
Alex van Andel
2fd25acc3c Phasing out prisma db push (#911) 2021-10-12 10:51:59 +00:00
Omar López
a73187d46b Makes fields with default values not null (#903)
* Makes fields with default values non-optional

* Removes PeriodType enum for now

* Adds missing migrations

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-12 11:09:39 +01:00
Alex Johansson
c3dc18643e /integrations facelift (#858) 2021-10-12 11:35:44 +02:00
Syed Ali Shahbaz
7dc4a55319 Bugfix CAL-566 guest emails missing (#905)
* removed empty dep from useCallback of bookingHandler

* added guestEmails as the dep instead of no dep

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-12 08:29:12 +00:00
Mihai C
f27b0b3cad fix: availability creation and expand/collapse settings (#882)
* fix: availability creation and expand/collapse settings

* additional fixes

* Update pages/api/availability/eventtype.ts

Co-authored-by: Alex Johansson <alexander@n1s.se>

* update branch

* fix: more fixes

Co-authored-by: Alex Johansson <alexander@n1s.se>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Mihai Colceriu <colceriumi@gmail.com>
2021-10-12 08:17:25 +00:00
Alex van Andel
683713e73f Fixes event type deletion (#906) 2021-10-11 20:56:03 +01:00
Bailey Pumfleet
69c808333e Fix URL field in event type page 2021-10-11 15:15:28 +01:00
Bailey Pumfleet
ff5b2b1668 Hotfix for PR #900: Remove unnecessary span tag 2021-10-11 14:53:14 +01:00
Bailey Pumfleet
6f219fc98c Fix for PR #900 where button tag is removed 2021-10-11 14:49:55 +01:00
Tiago Cruz
82f11b5121 Translations (#900)
* Translations

* Variables to Translated Words

* Fix Translations

* Fix Title to Create Events Types

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-11 14:42:43 +01:00
Alex Johansson
8cbf880af6 add locale to trpc context (#897) 2021-10-11 12:03:50 +00:00
Peer Richelsen
7488f29dc9 fixed event-type layout (#898)
* fixed event-type layout

* added props.centered to shell for centered layouts
2021-10-11 09:34:43 +00:00
Alex Johansson
0927b86831 Revert "Makes fields with default values non-optional (#892)" (#899)
This reverts commit bcf20914d3.
2021-10-11 10:30:09 +01:00
Peer Richelsen
9ef4815ffb wip 2021-10-11 09:32:58 +01:00
Omar López
bcf20914d3 Makes fields with default values non-optional (#892) 2021-10-10 17:48:21 +00:00
Peer Richelsen
fdd4bd2e14 changed sans font to roboto (#895) 2021-10-10 10:17:49 +00:00
Ciarán Hanrahan
e83980a999 UI updates to event types (#894) 2021-10-10 10:46:20 +01:00
Peer Richelsen
b794469c05 added cal logo to daily call (#888)
* added cal logo to daily call
2021-10-09 15:15:07 +00:00
Alex van Andel
014b74be8c Fixes custom-inputs clearing on save (#890) 2021-10-09 15:48:26 +01:00
Ciarán Hanrahan
f03a2c2a1a onboarding changes (#889) 2021-10-09 14:34:13 +01:00
Mihai C
35dd3f088c fix: move cancelled upcoming booking to cancelled tab (#883)
* fix: move cancelled upcoming booking to cancelled tab

* fix: lint

* Update server/routers/viewer.tsx

Co-authored-by: Mihai Colceriu <colceriumi@gmail.com>

* fix: also for past bookings

Co-authored-by: Alex Johansson <alexander@n1s.se>
2021-10-08 18:58:37 +03:00
Femi Odugbesan
015b7c18af fix #582: send user back to onboarding after adding integration (#635)
* fix #582: send user back to onboarding after adding integration if incomplete

* use more accurate, descriptive typings

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2021-10-08 14:10:57 +01:00
Mihai C
2c9b301b77 Feat/i18n crowdin (#752)
* feat: add crowdin and supported languages

* fix: main branch name

* feat: test crowdin integration

* feat: add crowdin config skeleton

* feat: update crowdin.yml

* fix: remove ro translation

* test: en translation

* test: en translation

* New Crowdin translations by Github Action (#735)

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

* test: en translation

* fix: separate upload/download workflows

* wip

* New Crowdin translations by Github Action (#738)

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

* wip

* wip

* wip

* wip

* wip

* typo

* wip

* wip

* update crowdin config

* update

* chore: support i18n de,es,fr,it,pt,ru,ro,en

* chore: extract i18n strings

* chore: extract booking components strings for i18n

* wip

* extract more strings

* wip

* fallback to getServerSideProps for now

* New Crowdin translations by Github Action (#874)

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

* fix: minor fixes on the datepicker

* fix: add dutch lang

* fix: linting issues

* fix: string

* fix: update GHA

* cleanup trpc

* fix linting

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-08 14:43:48 +03:00
Bailey Pumfleet
33a683d4b0 Add fix for create event type modal (#878)
Co-authored-by: Alex Johansson <alexander@n1s.se>
2021-10-07 22:49:53 +00:00
Alex Johansson
32567c8e80 fix readme heading (#879) 2021-10-07 21:59:56 +01:00
Lola
adee3fd211 Daily video calls (#542)
* ⬆️ Bump tailwindcss from 2.2.14 to 2.2.15

Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 2.2.14 to 2.2.15.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v2.2.14...v2.2.15)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* updating cal will provide a zoom meeting url

* updating cal will provide a zoom meeting url

* modifying how daily emails send

* modifying how daily emails send

* daily table

* migration updates

* daily table

* rebasing updates

* updating Daily references to a new table

* updating internal notes

* merge updates, adding Daily references to book/events.ts

* updated video email templates to remove Daily specific references

* updating the events.ts and refactoring in the event manager

* removing the package-lock

* changing calendso video powered by Daily.co to cal video powered by Daily.co

* updating some of the internal Daily notes

* added a modal for when the call/ link is invalid

* removing handle errors raw from the Daily video client

* prettier formatting fixes

* Added the Daily location to calendar events and updated Cal video references to Daily.co video

* updating references to create in event manager to check for Daily video

* fixing spacing on the cancel booking modal and adding Daily references in the event manager

* formatting fixes

* updating the readme file

* adding a daily interface in the event manager

* adding daily to the location labels

* added a note to cal event parser

* resolving yarn merge conflicts

* updating dailyReturn to DailyReturnType

* removing prettier auto and refactoring integrations: daily in the event manager

* removing changes to estlintrc.json

* updating read me formatting

* indent space for Daily ReadMe section

* resolving the merge conflicts in the yarn file

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lola-Ojabowale <lola.ojabowale@gmail.com>
2021-10-07 17:12:39 +01:00
Omar López
58de920951 Refactors custom input form & dialog (#853) 2021-10-07 15:43:20 +00:00
Omar López
30f97117e8 Revert "Revert "Feature/cal 274 add webhooks (#628)" (#854)" (#876)
This reverts commit 6868474c92.

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-07 15:14:47 +00:00
Alex Johansson
a9ee2ef9ae add playwright for e2e testing (#873) 2021-10-07 17:07:57 +02:00
Anton Podviaznikov
c62e1a0eeb Add more typescript types (#867) 2021-10-07 09:47:33 +01:00
Peer Richelsen
effbd44f48 added add to home screen banner for login in screens on mobile (#865) 2021-10-06 15:05:01 +01:00
Heaust Azure
521be467a4 resolved typescript problems for success.tsx, useTheme.tsx and event.ts (#786)
* resolved typescript problems for success.tsx, useTheme.tsx and event.ts

* remove NextRouter inferred type

Co-authored-by: Alex Johansson <alexander@n1s.se>

* remove router query inferred type

Co-authored-by: Alex Johansson <alexander@n1s.se>

* URIcomponent change ternary

Co-authored-by: Alex Johansson <alexander@n1s.se>

* infer types for event type

* completed requested changes

* Update pages/success.tsx | change context type to proper

Co-authored-by: Alex Johansson <alexander@n1s.se>

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex Johansson <alexander@n1s.se>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-10-05 23:53:24 +01:00
Omar López
95b49a5995 Several type fixes (#855)
* Several type fixes

* Update ee/lib/stripe/client.ts

Co-authored-by: Alex Johansson <alexander@n1s.se>

* Typo

* Refactors createPaymentLink

* Simplify calendarClietn type

Co-authored-by: Alex Johansson <alexander@n1s.se>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2021-10-05 16:46:48 -06:00
Anton Podviaznikov
4474e9dd74 Update api docs with /api/me (#862)
* add /api/me route to the docs

* change wording
2021-10-05 23:40:03 +01:00
Anton Podviaznikov
d272f32ee3 add zoom as location only if integration enabled, minor readme change (#860)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-10-05 19:14:11 +00:00
Alex Johansson
5254297944 bump @trpc/* (#859)
```bash
npx npm-check-updates --filter /@trpc/ -u  && yarn && git add .
```
2021-10-05 10:37:06 -06:00
Alex Johansson
d97da42950 make yarn dx work on Windows (#840)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-10-05 10:05:48 +00:00
Omar López
6868474c92 Revert "Feature/cal 274 add webhooks (#628)" (#854)
This reverts commit 4c07faefe7.
2021-10-05 02:30:00 +01:00
Syed Ali Shahbaz
4c07faefe7 Feature/cal 274 add webhooks (#628)
* added prisma models and migration, minor webhook init --WIP

* --WIP

* --WIP

* added radix-checkbox and other webhook additions --WIP

* added API connections and other modifications --WIP

* --WIP

* replaced checkbox with toggle --WIP

* updated to use Dialog instead of modal --WIP

* fixed API and other small fixes -WIP

* created a dummy hook for test --WIP

* replaced static hook with dynamic hooks

* yarn lock conflict quickfix

* added cancel event hook and other minor additions --WIP

* minor improvements --WIP

* added more add-webhook flow items--WIP

* updated migration to have alter table for eventType

* many ui/ux fixes, logic fixes and action fixes --WIP

* bugfix for incorrect webhook filtering

* some more fixes, edit webhook --WIP

* removed redundant checkbox

* more bugfixes and edit-webhook flow --WIP

* more build and lint fixes

* --WIP

* more fixes and added toast notif --WIP

* --updated iconButton

* clean-up

* fixed enabled check in edit webhook

* another fix

* fixed edit webhook bug

* added await to payload lambda

* wrapped payload call in promise

* fixed cancel/uid CTA alignment

* --requested changes --removed eventType relationship

* Adds missing migration

* Fixes missing daysjs plugin and type fixes

* Adds failsafe for webhooks

* Adds missing dayjs utc plugins

* Fixed schema and migrations

* Updates webhooks query

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
2021-10-04 17:40:52 -06:00
Peer Richelsen
785058558c added disabled email input box to settings, contact help@cal.com (#852) 2021-10-04 22:13:46 +01:00
Alex Johansson
abe4f38a5e <QueryCell /> type inference improvement (#849)
* asserts that `query.data` is never `null` when using `<QueryCell />`
2021-10-04 10:48:40 +01:00
Peer Richelsen
f70d92df7e added edit icon to event-type title input to make editing more obvious (#848) 2021-10-03 13:32:04 +01:00
Alex van Andel
89e5da15df Fixes weekStart & automatic theme adjust checkbox (#847) 2021-10-02 20:16:51 +00:00
Kelvin F. Alfaro
1662c9cf91 Correct License banner instructions. (#846)
* Update common.json

* Banner instructions were incorrect. 

Removed the I from "I Agree". License banner indicates setting NEXT_PUBLIC_LICENSE_CONSENT env var to "I Agree". Env var NEXT_PUBLIC_LICENSE_CONSENT set to only accept "agree"

* Update common.json

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2021-10-02 20:37:10 +01:00
Chris
33273b18d3 Use id to look up user (#843) 2021-10-02 17:53:13 +01:00
Alex Johansson
eb93e778bd simplify /bookings/[status] logic (#845) 2021-10-02 13:29:26 +00:00
Omar López
342ea3e5d2 Adds specfific empty copy by status (#842)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-10-02 12:08:35 +01:00
Alex van Andel
e12c879242 Bugfix/image upload fuzzy (#844) 2021-10-02 12:04:47 +01:00
Alex Johansson
6547ef1e86 make cypress more fail-safe (#836) 2021-10-01 16:37:11 +01:00
Alex Johansson
4879479981 add <QueryCell /> to remove bespoke loading/error/empty/success logic (#835) 2021-10-01 15:12:47 +00:00
Alex Johansson
5bed09218a add some query client defaults (#838) 2021-10-01 15:33:14 +01:00
Alex Johansson
08f83dd85c fix login sometimes having to login twice (#839) 2021-10-01 13:20:28 +01:00
Peer Richelsen
3d3e99272a replaced another location.hostname, deleted DonateBanner (#834)
* replaced another location.hostname, deleted DonateBanner

* wip
2021-10-01 09:44:53 +00:00
Peer Richelsen
860db6c959 added license banner (#833)
* added license banner and NEXT_PUBLIC_LICENSE_CONSENT in .env.example. agree to hide the banner

* minor changes to license banner

* added /ee/ to tailwind purge config

* wip
2021-10-01 00:42:08 +01:00
Alex Johansson
30163f0a78 refactor availability times form using react-hook-form (#824)
* use toast

* use `Button`

* make fn

* rewrite with react-hook-form

* add comment

* fix deps

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-09-30 22:37:52 +01:00
Alex Johansson
60298f6eeb resize image before storing (#831)
* resize image fn

* wip

* simplify transform

* comment

* fix

* rm log

* allow pass-through for non-base-64

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2021-09-30 21:37:29 +01:00
Peer Richelsen
e33962686e minor changes to booking page and navtabs (#832) 2021-09-30 20:59:34 +01:00
Alex Johansson
c80992aa1c move withTRPC-HOC to _app.tsx (#822) 2021-09-30 06:28:30 -06:00
Peer Richelsen
8ac56fbf4a bring back h-screen in user page (#829) 2021-09-30 11:41:13 +00:00
Peer Richelsen
378cf25521 turned teams page into flex from grid to center it (#828)
* turned teams page into flex from grid to center it, removed h-screen from user page

* new icon button

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2021-09-30 12:31:04 +01:00
Omar López
dc6841e761 Bookings UI improvements (#826) 2021-09-30 11:46:39 +01:00
Bailey Pumfleet
033c4835f7 Fix: Remove 0s from team page (#827)
* Remove 0s from team page

* damn it
2021-09-30 10:24:02 +01:00
Omar López
a04336ba06 Added booking tabs, type fixing and refactoring (#825)
* More type fixes

* More type fixes

* Type fixes

* Adds inputMode to email fields

* Added booking tabs

* Adds aditional notes to bookings
2021-09-29 22:33:18 +01:00
Syed Ali Shahbaz
5318047794 fixed reload after disband team (#823) 2021-09-29 18:04:47 +01:00
Bailey Pumfleet
079a920c2c CTA on success page (#818) 2021-09-29 15:36:29 +00:00
Alex van Andel
a30381f229 Lowercase username try #3 (#821)
3 times is a charm.. can't believe this took 3 tries.
2021-09-29 15:58:25 +01:00
Omar López
e7314257c3 Adds possibilty to analyze client and server bundle size (#677) 2021-09-29 15:36:58 +01:00
Mihai C
4a07c27da5 fix: alert severity (#814)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-09-29 11:33:40 +00:00
Peer Richelsen
d7df292296 replaced favicons (#817) 2021-09-29 10:57:19 +00:00
dependabot[bot]
3014df61cc ⬆️ Bump tailwindcss from 2.2.15 to 2.2.16 (#796)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 2.2.15 to 2.2.16.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v2.2.15...v2.2.16)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-29 11:28:27 +01:00
Mihai C
8f6689cfc1 chore: add some dx configs (#650)
* chore: add some dx configs

* fix: update configs

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-09-29 09:41:38 +00:00
Omar López
7a8ad8381e Fixes error when user cancels stripe integration (#813)
* Fixes customer portal return ur

* Attempt to fix portal CSRF issue

* Fixes CSRF issue with Stripe redirect

* Fixes error when user cancels stripe integration
2021-09-28 22:27:06 +01:00
Alex Johansson
f8a4f81991 fix stripe customer.subscription.deleted-webhook (#811) 2021-09-28 19:48:45 +00:00
Alex Johansson
e684824c79 yarn lint --fix (#812) 2021-09-28 20:12:48 +01:00
Malte Delfs
3b71c86b1e prevent cancellation of past events (#568)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
Co-authored-by: Peer_Rich <peeroke@gmail.com>
2021-09-28 18:23:50 +01:00
Omar López
230c82e316 Zomars/fixes stripe portal redirect (#808) 2021-09-28 17:21:45 +01:00
Peer Richelsen
b8c4dfb9e1 minor style changes to billing page 2021-09-28 15:07:33 +01:00
Alex Johansson
a4fbe7b2b4 downgrade users when trial ends (#767)
* wip

* wip

* wip

* wtf

* should be all the logic

* comment

* fix receiver name

* safeguard a bit more

* downgrade users cron job

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-09-28 14:00:19 +01:00
Bailey Pumfleet
0372289fe6 Unblock / on robots.txt 2021-09-28 13:21:30 +01:00
Bailey Pumfleet
5478c135d1 Set tabIndex for forgot password & add loader (#807) 2021-09-28 10:43:15 +00:00
Bailey Pumfleet
a3abdac33b Update billing tab UI (#806) 2021-09-28 11:27:46 +01:00
Alex Johansson
7779c098dc confirming event gives no visual (#803) 2021-09-28 09:16:02 +00:00
Omar López
dd9f801872 cal 485 prevent users from changing their username to premium ones (#799)
* Makes userRequired middleware

* Prevent users from changing usernames to premium ones

* refactor on zomars' branch (#801)

* rename `profile` -> `mutation`

* `createProtectedRouter()` helper

* move profile mutation to `viewer.`

* simplify checkUsername

* Auto scrolls to error when there is one

* Renames username helpers

* Follows db convention

Co-authored-by: Alex Johansson <alexander@n1s.se>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-09-28 09:57:30 +01:00
Omar López
f23e4f2b9d Fixes customer portal return url (#802) 2021-09-28 09:04:30 +01:00
Alex Johansson
dcea723ea4 confirming event gives no visual feedback (#804) 2021-09-27 18:11:52 -06:00
Peer Richelsen
58dde562a3 switching to stripes customer portal (#772) 2021-09-27 23:57:23 +01:00
Alex Johansson
9f2cfffce4 fix loading spinner flicker (#800) 2021-09-27 23:42:04 +01:00
Alex van Andel
dc7b084bdf Remove Moment dependency by changing implementation of DateRangePicker to something leaner (#798)
* chore: extracted all DateRangePicker logic from event-types/[type].tsx

* Minor alignment fixes + Date.now() instead of 1970.

* Removes react-dates, implements wojtekmaj/react-daterange-picker
2021-09-27 22:12:55 +01:00
Syed Ali Shahbaz
78c78a6981 added relative position to placeholder parent (#797) 2021-09-27 18:59:50 +01:00
dependabot[bot]
35c450d5ef ⬆️ Bump jest from 27.2.1 to 27.2.2 (#785)
Bumps [jest](https://github.com/facebook/jest) from 27.2.1 to 27.2.2.
- [Release notes](https://github.com/facebook/jest/releases)
- [Changelog](https://github.com/facebook/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/jest/compare/v27.2.1...v27.2.2)

---
updated-dependencies:
- dependency-name: jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-09-27 17:04:32 +00:00
Alex Johansson
649e79bdc7 statically render profile pages (#615) 2021-09-27 17:09:19 +01:00
Alex Johansson
34300650e4 add tRPC (#614)
* add trpc

* trpc specific

* fix deps

* lint fix

* upgrade prisma

* nativeTypes

* nope, not needed

* fix app propviders

* Revert "upgrade prisma"

This reverts commit e6f2d2542a01ec82c80aa2fe367ae12c68ded1a5.

* rev

* up trpc

* simplify

* wip - bookings page with trpc

* bookings using trpc

* fix `Shell` props

* call it viewerRouter instead

* cleanuop

* ssg helper

* fix lint

* fix types

* skip

* add `useRedirectToLoginIfUnauthenticated`

* exhaustive-deps

* fix callbackUrl

* rewrite `/availability` using trpc

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-09-27 14:47:55 +00:00
Peer Richelsen
0938f6f4b2 added z-index for event types dropdown (#791) 2021-09-27 10:19:11 +00:00
Alex van Andel
c22beb698c Fixes the Radix UI warning for consistent IDs (#774)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-09-26 21:55:27 +00:00
Omar López
7ab49acebe Fixes eventype form (#777)
* Type fixes

* Uses all integrations and session fixes on getting started page

* eventtype form fixes

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

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-09-26 21:49:16 +00:00
Alex Johansson
b23c032a4c Update .kodiak.toml (#782)
maybe this can stop @PeerRich & @baileypumfleet to use the force squash? 

everything you open will be automatically approved so you can use "Auto-merge" at least when you yolo stuff

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-09-26 22:45:08 +01:00
Alex Johansson
22b050b9e7 yarn lint --fix (#783) 2021-09-26 14:08:40 +00:00
Peer Richelsen
6d2f89fc32 Cale to Cal 2021-09-26 15:04:01 +01:00
Peer Richelsen
ee5cc32936 renamed PoweredByCalendso 2021-09-25 17:03:08 +01:00
Peer Richelsen
b125ece57b gave powered by absolute path 2021-09-25 16:55:13 +01:00
Chris
97cb7bdc83 Show raw secret during two-factor setup (#775) 2021-09-25 16:53:12 +01:00
Alex van Andel
515c548acd Fixes theme flickering on booking & availability select page (#771) 2021-09-24 23:11:30 +01:00
Eduardo M
727793af02 Fix link ignoring app url on even-type (#773) 2021-09-24 23:09:30 +01:00
Omar López
9e7cb2c0b8 CAL-469 Adds intercom dynamically (#762)
* Adds react-use-intercom

* Adds intercom env var

* Loads intercom dynamically if env is set

* CAL-473 Fixes client-side routing for authed pages

* Moves intercom code to ee

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-09-24 21:02:03 +01:00
Peer Richelsen
889b3f36ae fixed mobile booking layout for long descriptions 2021-09-24 18:03:09 +01:00
Omar López
50367c236a Prevents a 404 page for payments (#770) 2021-09-24 17:45:35 +01:00
Peer Richelsen
63930c1817 Fix/event type button alignment (#765)
* fixed event-type buttons on mobile, added more font-cal classes

* remove 1-on-1 on mobile event types

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-09-24 15:11:15 +00:00
Alex Johansson
6a4d6c7eba remove npm-run-all (#768) 2021-09-24 15:07:31 +00:00
Peer Richelsen
a8ec6e7060 replaced window.location with NEXT_PUBLIC_APP_URL (#769)
* replaced window.location with process.env.NEXT_PUBLIC_APP_URL, removed empty classNames

* wip
2021-09-24 09:03:28 -06:00
Alex Johansson
f709972f86 ui bug: alert looking funky (#766)
* tweak alert text to be growing
* fix alert looking funky
* get rid of `typeof window` dirt
2021-09-24 06:52:00 -06:00
Peer Richelsen
bb1da8150f fixed event-type buttons on mobile, added more font-cal classes (#764) 2021-09-24 11:28:57 +00:00
Peer Richelsen
c152e43b82 minor changes to event type edit input 2021-09-24 12:23:06 +01:00
Peer Richelsen
521bb4069a Merge branch 'main' of github.com:calendso/calendso 2021-09-24 11:38:18 +01:00
Alex van Andel
2b2fde179a Fixes #701: User's page shows Team events with broken links (#761) 2021-09-24 10:23:08 +00:00
Omar López
420daec147 CAL-473 Fixes client-side routing for authed pages (#763) 2021-09-24 11:16:46 +01:00
Peer Richelsen
f5a7ed2e36 minor changes to powered by badge 2021-09-23 23:56:13 +01:00
Peer Richelsen
d3d6778c60 Merge branch 'main' of github.com:calendso/calendso 2021-09-23 23:53:04 +01:00
Peer Richelsen
8ad685653c new apple calendar and stripe icon 2021-09-23 23:50:18 +01:00
Bailey Pumfleet
9e785c01fd The tiniest linting fix ever 2021-09-23 23:31:16 +01:00
Alex van Andel
ad3a06384f Fixes a crash caused by too many redraws (#759) 2021-09-23 23:30:09 +01:00
Peer Richelsen
b741559dbc added more font-cal classes 2021-09-23 21:48:27 +01:00
Peer Richelsen
235e74440c hotfix for powered by badge (#758)
* minor fix to powered by

* fixed powered by logo
2021-09-23 21:19:11 +01:00
Peer Richelsen
6dfd3f4aba minor fix to powered by (#757) 2021-09-23 21:05:24 +01:00
Omar López
2c50781084 Makes every day available for events that don't have availability set (#756)
* Abstracts MinutesField

* Adds missing Minimum booking notice

* Refactoring

* Fixes int field sent as string

* Sorts slots by time

* Fixes availability page

* Fixes available days

* Type fixes

* More availability bugfixes

* Makes every day available for events that don't have availability set

* Type fixes
2021-09-23 12:03:07 -06:00
Omar López
bcacc1d166 More availability fixes (#755)
* Abstracts MinutesField

* Adds missing Minimum booking notice

* Refactoring

* Fixes int field sent as string

* Sorts slots by time

* Fixes availability page

* Fixes available days

* Type fixes

* More availability bugfixes
2021-09-23 18:18:29 +01:00
Alex Johansson
790ed3e6b1 fix: branding is always disabled for pro (#754)
* add jwt secret

* fix `isBrandingHidden`

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-09-23 15:25:34 +00:00
Mihai C
7574c322c4 Revert "GHA for crowdin (WIP) (#731)" (#751)
This reverts commit 168db02e1f.
2021-09-23 15:28:50 +01:00
Mihai C
168db02e1f GHA for crowdin (WIP) (#731)
* feat: add crowdin and supported languages

* fix: main branch name

* feat: test crowdin integration
2021-09-23 14:22:49 +00:00
Omar López
cb4a1e031e Fixes user event availability page (#749)
* Abstracts MinutesField

* Adds missing Minimum booking notice

* Refactoring

* Fixes int field sent as string

* Sorts slots by time

* Fixes availability page

* Fixes available days
2021-09-23 15:08:44 +01:00
Alex Johansson
4d2e556d7d fix CI e2e tests (#748) 2021-09-23 14:11:58 +01:00
Nico
2bc4678ef0 Fixed bug that old credentials were used to create a zoom booking (#729)
Co-authored-by: nicolas <privat@nicolasjessen.de>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-09-23 10:35:39 +01:00
Bailey Pumfleet
bb3362f2ef Add JWT secret to next-auth.js (#730) 2021-09-23 10:02:53 +01:00
Mihai C
82e7e51fca Setup i18n and locale detection (#712)
* feat: setup translations

* feat: i18n setup

* Update pages/settings/profile.tsx

Co-authored-by: Alex Johansson <alexander@n1s.se>

* fix: abstract locale hook

* fix: set default locale if preferred locale is not supported

* Revert "fix: set default locale if preferred locale is not supported"

This reverts commit e2a3d81371ee02a033520058a1d7d61cffeffc94.

* fix: set default locale if preferred locale is not supported

* fix: use 1 namespace and remove unnecessary logs

* fix: yarn.lock

* fix: linting errors

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Alex Johansson <alexander@n1s.se>
2021-09-23 09:49:17 +01:00
Nico
3764a9d462 Bugfix/include zoom location (#715)
* Store video data in event location; fixed several types

* fixed malformed id

* Insert Zoom data when updating as well

* Add columns to store (video) meetings

* Store meeting data

* fixed type

* Use stored videoCallData

* Store location in field as well

* Use meta field for booking reference

* Introduced meta field in code

* Revert "Introduced meta field in code"

This reverts commit 535baccee3d87e3e793e84c4b91f8cad0e09063f.

* Revert "Use meta field for booking reference"

This reverts commit 174c252f672bcc3e461c8b3b975ac7541066d6a8.

* Linting fixes

Co-authored-by: nicolas <privat@nicolasjessen.de>
Co-authored-by: Peer_Rich <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-09-22 23:43:10 +01:00
Peer Richelsen
4f964533cf closes #726 (#728) 2021-09-22 21:38:20 +00:00
Peer Richelsen
51752bd2bd added Cal Sans (#709)
* added Cal Sans

* Delete EmptyScreen.tsx
2021-09-22 21:23:19 +00:00
Omar López
d194878bb2 Suggestion: let prettier sort imports order (#673)
* Suggestion: let prettier sort imports order

# Conflicts:
#	yarn.lock

* AUTO SORT ALL THE IMPORTS

* Linting

* Fixes test
2021-09-22 13:52:38 -06:00
Omar López
3add84a279 Adds Stripe integration (#717)
* Adds Stripe integration

* Moves Stripe instrucctions to ee

* Adds NEXT_PUBLIC_APP_URL variable

* Adds fallback for NEXT_PUBLIC_APP_URL

* Throws error objects instead

* Improved error handling

* Removes deprecated method

* Bug fixing

* Payment refactoring

* PaymentPage fixes

* Fixes preview links

* More preview link fixes

* Fixes client links

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2021-09-22 19:36:13 +01:00
Peer Richelsen
43563bc8d5 made it easier to see if there are more times available to scroll (#722)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-09-22 14:44:38 +00:00
Peer Richelsen
81a3d82ce7 readme: added gif demo of product vs screenshot 2021-09-22 15:36:19 +01:00
Peer Richelsen
0b74ef35d2 updated README shields 2021-09-22 15:25:42 +01:00
Peer Richelsen
a280739f01 MIT to AGPLv3 2021-09-22 15:22:36 +01:00
Omar López
e1f1386332 Feat disable guests for events (#719)
* Abstracts CheckboxField

* Allows disabling the guests field while booking

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-09-22 12:04:32 +01:00
Chris
1c2998fc13 Ensure users cannot delete teams they don’t own (#720)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-09-22 11:43:32 +01:00
Heaust Azure
8eb3a31af4 Default to slug when Full Name isn't set (#721)
When Full Name isn't set and a new team is created,
The part of the now drop-down add new event type (previously button)
that's supposed to be personal or non-team
appears as an empty div.
This commit makes it default to slug / username in such a case.
2021-09-22 11:43:08 +01:00
Femi Odugbesan
a047177e72 Fix/duplicate events on onboarding (#716) 2021-09-22 08:25:33 +01:00
Omar López
d4f29464f2 Fixes deployed previews (#714) 2021-09-21 21:18:41 +01:00
Omar López
48bc4c64f4 [CAL-463] Validates required checkboxes (#713) 2021-09-21 20:42:44 +01:00
Peer Richelsen
3c089af58a new EmptyScreen component, using it in /bookings (#708)
* new EmptyScreen component, using it in /bookings

* Linting fixes

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2021-09-21 11:36:29 +01:00
434 changed files with 22306 additions and 10009 deletions

View File

@@ -1,3 +0,0 @@
{
"presets": ["next/babel"]
}

View File

@@ -1,8 +1,21 @@
# Set this value to 'agree' to accept our license:
# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
#
# Summary of terms:
# - The codebase has to stay open source, whether it was modified or not
# - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by emailing: license@cal.com
NEXT_PUBLIC_LICENSE_CONSENT=''
# DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>/<db-name>'
DATABASE_URL="postgresql://postgres:@localhost:5432/calendso?schema=public"
GOOGLE_API_CREDENTIALS='secret'
BASE_URL='http://localhost:3000'
NEXT_PUBLIC_APP_URL='http://localhost:3000'
JWT_SECRET='secret'
# @see: https://github.com/calendso/calendso/issues/263
# Required for Vercel hosting - set NEXTAUTH_URL to equal your BASE_URL
@@ -19,6 +32,10 @@ MS_GRAPH_CLIENT_SECRET=
ZOOM_CLIENT_ID=
ZOOM_CLIENT_SECRET=
#Used for the Daily integration
DAILY_API_KEY=
DAILY_SCALE_PLAN=''
# E-mail settings
# Cal uses nodemailer (@see https://nodemailer.com/about/) to provide email sending. As such we are trying to
@@ -37,6 +54,17 @@ EMAIL_SERVER_PASSWORD='<office365_password>'
# ApiKey for cronjobs
CRON_API_KEY='0cc0e6c35519bba620c9360cfe3e68d0'
# Stripe Config
NEXT_PUBLIC_STRIPE_PUBLIC_KEY= # pk_test_...
STRIPE_PRIVATE_KEY= # sk_test_...
STRIPE_CLIENT_ID= # ca_...
STRIPE_WEBHOOK_SECRET= # whsec_...
PAYMENT_FEE_PERCENTAGE=0.005 # Take 0.5% commission
PAYMENT_FEE_FIXED=10 # Take 10 additional cents commission
# Application Key for symmetric encryption and decryption
# must be 32 bytes for AES256 encryption algorithm
CALENDSO_ENCRYPTION_KEY=
# Intercom Config
NEXT_PUBLIC_INTERCOM_APP_ID=

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -25,9 +25,12 @@
},
"overrides": [
{
"files": ["cypress/**/*.js"],
"files": ["playwright/**/*.{js,jsx,tsx,ts}"],
"rules": {
"no-undef": "off"
"no-undef": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-implicit-any": "off"
}
}
],

View File

@@ -1,18 +1,19 @@
---
name: Bug report
about: Report any issues with the platform
title: ''
title: ""
labels: bug
assignees: ''
assignees: ""
---
Found a bug? Please fill out the sections below. 👍
### Issue Summary
A summary of the issue. This needs to be a clear detailed-rich summary.
A summary of the issue. This needs to be a clear detailed-rich summary.
### Steps to Reproduce
1. (for example) Went to ...
2. Clicked on...
3. ...
@@ -20,6 +21,7 @@ A summary of the issue. This needs to be a clear detailed-rich summary.
Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?
### Technical details
* Browser version: You can use https://www.whatsmybrowser.org/ to find this out.
* Node.js version
* Anything else that you think could be an issue.
- Browser version: You can use https://www.whatsmybrowser.org/ to find this out.
- Node.js version
- Anything else that you think could be an issue.

View File

@@ -1,36 +1,43 @@
---
name: Feature request
about: Suggest a feature or idea
title: ''
title: ""
labels: enhancement
assignees: ''
assignees: ""
---
> Please check if your Feature Request has not been already raised in the [Discussions Tab](https://github.com/calendso/calendso/discussions), as we would like to reduce duplicates. If it has been already raised, simply upvote it 🔼.
### Is your proposal related to a problem?
<!--
Provide a clear and concise description of what the problem is.
For example, "I'm always frustrated when..."
-->
(Write your answer here.)
### Describe the solution you'd like
<!--
Provide a clear and concise description of what you want to happen.
-->
(Describe your proposed solution here.)
### Describe alternatives you've considered
<!--
Let us know about other solutions you've tried or researched.
-->
(Write your answer here.)
### Additional context
<!--
Is there anything else you can add about the proposal?
You might want to link to related issues here, if you haven't already.
-->
(Write your answer here.)

View File

@@ -1,9 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
commit-message:
prefix: "⬆️"
open-pull-requests-limit: 1

View File

@@ -4,6 +4,19 @@ jobs:
build:
name: Build on Node ${{ matrix.node }} and ${{ matrix.os }}
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
NODE_ENV: test
BASE_URL: http://localhost:3000
JWT_SECRET: secret
services:
postgres:
image: postgres:12.1
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
ports:
- 5432:5432
runs-on: ${{ matrix.os }}
strategy:
matrix:
@@ -28,5 +41,31 @@ jobs:
path: ${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs
- run: yarn prisma migrate deploy
- run: yarn test
- run: yarn build
types:
name: Check types
strategy:
matrix:
node: ["14.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repo
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- name: Install deps
uses: bahmutov/npm-install@v1
- run: yarn check-changed-files

View File

@@ -0,0 +1,23 @@
name: Cron - bookingReminder
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
jobs:
cron:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/bookingReminder \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail

View File

@@ -0,0 +1,23 @@
name: Cron - downgradeUsers
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
schedule:
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
- cron: "0,15,30,45 * * * *"
jobs:
cron:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_API_KEY }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/downgradeUsers \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
--fail

25
.github/workflows/crowdin.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Crowdin Action
on:
push:
branches:
- main
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: crowdin action
uses: crowdin/github-action@1.4.0
with:
upload_translations: true
download_translations: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -7,8 +7,9 @@ jobs:
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
NODE_ENV: test
BASE_URL: http://localhost:3000
JWT_SECRET: secret
GOOGLE_API_CREDENTIALS: "{}"
# GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
# CRON_API_KEY: xxx
# CALENDSO_ENCRYPTION_KEY: xxx
@@ -50,14 +51,29 @@ jobs:
uses: actions/cache@v2
with:
path: ${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-nextjs
- run: yarn build
- run: yarn test
- run: yarn prisma migrate deploy
- run: yarn db-seed
- run: yarn build
- run: yarn start &
- run: npx wait-port 3000 --timeout 10000
- run: yarn cypress run
- name: Cache playwright binaries
uses: actions/cache@v2
id: playwright-cache
with:
path: |
~/Library/Caches/ms-playwright
~/.cache/ms-playwright
**/node_modules/playwright
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
- name: Install playwright deps
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install-deps
- run: yarn test-playwright
- name: Upload videos
if: ${{ always() }}
@@ -65,5 +81,5 @@ jobs:
with:
name: videos
path: |
cypress/videos
cypress/screenshots
playwright/screenshots
playwright/videos

6
.gitignore vendored
View File

@@ -11,6 +11,9 @@
# testing
/coverage
.nyc_output
playwright/videos
playwright/screenshots
# next.js
/.next/
@@ -51,6 +54,3 @@ yarn-error.log*
# Local History for Visual Studio Code
.history/
cypress/videos
cypress/screenshots

View File

@@ -4,4 +4,5 @@ version = 1
autoupdate_label = "♻️ autoupdate"
[approve]
auto_approve_usernames = ["dependabot"]
auto_approve_usernames = ["dependabot", "PeerRich", "baileypumfleet"]

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
# used in tandem with package.json engine to only enable yarn
engine-strict=true

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
14.17

View File

@@ -3,4 +3,13 @@ node_modules
public
**/**/node_modules
**/**/.next
**/**/public
**/**/public
*.lock
*.log
.gitignore
.npmignore
.prettierignore
.DS_Store
.eslintignore

View File

@@ -7,4 +7,6 @@ module.exports = {
semi: true,
printWidth: 110,
arrowParens: "always",
importOrder: ["^@ee/(.*)$", "^@lib/(.*)$", "^@components/(.*)$", "^@(server|trpc)/(.*)$", "^[./]"],
importOrderSeparation: true,
};

1
.vercelignore Normal file
View File

@@ -0,0 +1 @@
.github

View File

@@ -4,6 +4,7 @@
"esbenp.prettier-vscode", // prettier plugin
"dbaeumer.vscode-eslint", // eslint plugin
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
"heybourn.headwind" // automatically sort tailwind classes in predictable order, kinda like "prettier for tailwind"
"heybourn.headwind", // automatically sort tailwind classes in predictable order, kinda like "prettier for tailwind",
"stripe.vscode-stripe" // stripe VSCode extension
]
}

15
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@@ -5,5 +5,9 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.run": "onSave"
"eslint.run": "onSave",
"workbench.colorCustomizations": {
"titleBar.activeBackground": "#292929",
"titleBar.inactiveBackground": "#888888"
}
}

View File

@@ -13,7 +13,7 @@
<a href="https://cal.com"><strong>Learn more »</strong></a>
<br />
<br />
<a href="https://join.slack.com/t/calendso/shared_invite/zt-mem978vn-RgOEELhA5bcnoGONxDCiHw">Slack</a>
<a href="https://cal.com/slack">Slack</a>
·
<a href="https://cal.com">Website</a>
·
@@ -26,17 +26,18 @@
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
<a href="https://github.com/calendso/calendso/stargazers"><img src="https://img.shields.io/github/stars/calendso/calendso" alt="Github Stars"></a>
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a>
<img src="https://img.shields.io/github/license/calendso/calendso" alt="License">
<a href="https://github.com/calendso/calendso/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
<img src="https://img.shields.io/github/package-json/v/calendso/calendso">
<a href="https://github.com/calendso/calendso/pulse"><img src="https://img.shields.io/github/commit-activity/m/calendso/calendso" alt="Commits-per-month"></a>
<a href="https://cal.com/pricing"><img src="https://img.shields.io/badge/Pricing-%2412%2Fmonth-brightgreen" alt="Pricing"></a>
<a href="https://cal.crowdin.com/Cal"><img src="https://badges.crowdin.net/e/5a55420475b48696779e30e0208a1899/localized.svg" alt="Translate Slack"></a>
</p>
<!-- ABOUT THE PROJECT -->
## About The Project
<img width="100%" alt="booking-screen" src="https://user-images.githubusercontent.com/8019099/133429837-69ac8554-4c9c-43f9-90dd-c3337002d8ff.png">
<img width="100%" alt="booking-screen" src="https://user-images.githubusercontent.com/8019099/134363898-4b29e18f-3e61-42b7-95bc-10891056249d.gif">
# Scheduling infrastructure for absolutely everyone.
@@ -82,8 +83,9 @@ Here is what you need to be able to run Cal.
You will also need Google API credentials. You can get this from the [Google API Console](https://console.cloud.google.com/apis/dashboard). More details on this can be found below under the [Obtaining the Google API Credentials section](#Obtaining-the-Google-API-Credentials).
### Development Setup
## Development
### Setup
#### Quick start with `yarn dx`
> - **Requires Docker to be installed**
@@ -143,7 +145,7 @@ yarn dx
5. Set up the database using the Prisma schema (found in `prisma/schema.prisma`)
```sh
npx prisma db push
npx prisma migrate deploy
```
6. Run (in development mode)
```sh
@@ -158,6 +160,15 @@ yarn dx
10. Open a browser to [http://localhost:3000](http://localhost:3000) and login with your just created, first user.
11. Set a 32 character random string in your .env file for the CALENDSO_ENCRYPTION_KEY.
### E2E-Testing
```bash
# In first terminal
yarn dx
# In second terminal
yarn test-playwright
```
### Upgrading from earlier versions
1. Pull the current version:
@@ -206,8 +217,11 @@ yarn dx
### Docker
The Docker configuration for Cal is an effort powered by people within the community. Cal.com, Inc. does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk.
If you want to contribute to the Docker repository, [reply here](https://github.com/calendso/docker/discussions/32).
The Docker configuration can be found [in our docker repository](https://github.com/calendso/docker).
### Railway
@@ -240,7 +254,7 @@ Contributions are what make the open source community such an amazing place to b
2. In the search box, type calendar and select the Google Calendar API search result.
3. Enable the selected API.
4. Next, go to the [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) from the side pane. Select the app type (Internal or External) and enter the basic app details on the first page.
5. In the second page on Scopes, select Add or Remove Scopes. Search for Calendar.event and select the scope with scope value `.../auth/calendar.events`, `.../auth/calendar.readonly`, `.../auth/calendar` and select Update.
5. In the second page on Scopes, select Add or Remove Scopes. Search for Calendar.event and select the scope with scope value `.../auth/calendar.events`, `.../auth/calendar.readonly` and select Update.
6. In the third page (Test Users), add the Google account(s) you'll using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured.
7. Now select [Credentials](https://console.cloud.google.com/apis/credentials) from the side pane and then select Create Credentials. Select the OAuth Client ID option.
8. Select Web Application as the Application Type.
@@ -268,16 +282,24 @@ Contributions are what make the open source community such an amazing place to b
7. Click "Create".
8. Now copy the Client ID and Client Secret to your .env file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields.
9. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs.
10. Also add the redirect URL given above as a whitelist URL and enable "Subdomain check". Make sure, it says "saved" below the form.
10. Also add the redirect URL given above as a allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
11. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
12. Click "Done".
13. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings.
## Obtaining Daily API Credentials
1. Open [Daily](https://www.daily.co/) and sign into your account.
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
3. Copy your API key.
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
5. If you have a [Daily Scale Plan](https://www.daily.co/pricing) can also enable the ability to record Daily video meetings. To do so, set the `DAILY_SCALE_PLAN` environment variable to `'true'`
<!-- LICENSE -->
## License
Distributed under the MIT License. See `LICENSE` for more information.
Distributed under the AGPLv3 License. See `LICENSE` for more information.
<!-- ACKNOWLEDGEMENTS -->

View File

@@ -7,20 +7,20 @@ info:
email: support@cal.com
license:
name: MIT License
url: 'https://opensource.org/licenses/MIT'
url: "https://opensource.org/licenses/MIT"
version: 1.0.0
termsOfService: 'https://cal.com/terms'
termsOfService: "https://cal.com/terms"
server:
url: 'http://localhost:{port}'
url: "http://localhost:{port}"
description: Local Development Server
variables:
port:
default: '3000'
default: "3000"
tags:
- name: Authentication
description: 'Auth routes, powered by Next-Auth.js'
description: "Auth routes, powered by Next-Auth.js"
externalDocs:
url: 'http://next-auth.js.org/'
url: "http://next-auth.js.org/"
- name: Availability
description: Checking and setting user availability
- name: Booking
@@ -38,15 +38,15 @@ paths:
summary: Displays the sign in page
tags:
- Authentication
'/api/auth/signin/:provider':
"/api/auth/signin/:provider":
post:
description: Starts an OAuth signin flow for the specified provider. The POST submission requires CSRF token from /api/auth/csrf.
summary: Starts an OAuth signin flow for the specified provider
tags:
- Authentication
'/api/auth/callback/:provider':
"/api/auth/callback/:provider":
get:
description: 'Handles returning requests from OAuth services during sign in. For OAuth 2.0 providers that support the state option, the value of the state parameter is checked against the one that was generated when the sign in flow was started - this uses a hash of the CSRF token which MUST match for both the POST and GET calls during sign in.'
description: "Handles returning requests from OAuth services during sign in. For OAuth 2.0 providers that support the state option, the value of the state parameter is checked against the one that was generated when the sign in flow was started - this uses a hash of the CSRF token which MUST match for both the POST and GET calls during sign in."
summary: Handles returning requests from OAuth services
tags:
- Authentication
@@ -103,26 +103,26 @@ paths:
summary: Reset a user's password
tags:
- Authentication
'/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}':
"/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}":
get:
description: 'Gets the busy times for a particular user, by username.'
description: "Gets the busy times for a particular user, by username."
summary: Gets the busy times for a user
tags:
- Availability
responses:
'200':
"200":
description: OK
content:
application/json:
schema:
type: array
description: ''
description: ""
minItems: 1
uniqueItems: true
x-examples:
example-1:
- start: 'Fri, 03 Sep 2021 17:00:00 GMT'
end: 'Fri, 03 Sep 2021 17:40:00 GMT'
- start: "Fri, 03 Sep 2021 17:00:00 GMT"
end: "Fri, 03 Sep 2021 17:40:00 GMT"
items:
type: object
properties:
@@ -135,7 +135,7 @@ paths:
required:
- start
- end
'500':
"500":
description: Internal Server Error
parameters:
- schema:
@@ -163,13 +163,13 @@ paths:
tags:
- Availability
responses:
'200':
"200":
description: OK
content:
application/json:
schema:
type: array
description: ''
description: ""
minItems: 1
uniqueItems: true
items:
@@ -221,7 +221,7 @@ paths:
externalId: c_feunmui1m8el5o1oo885fu48k8@group.calendar.google.com
integration: google_calendar
name: 1.0 Launch
'500':
"500":
description: Internal Server Error
post:
description: Adds a selected calendar for the user.
@@ -229,7 +229,7 @@ paths:
tags:
- Availability
responses:
'200':
"200":
description: OK
content:
application/json:
@@ -238,7 +238,7 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
requestBody:
content:
@@ -256,7 +256,7 @@ paths:
tags:
- Availability
responses:
'200':
"200":
description: OK
content:
application/json:
@@ -265,7 +265,7 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
requestBody:
content:
@@ -284,7 +284,7 @@ paths:
tags:
- Availability
responses:
'200':
"200":
description: OK
content:
application/json:
@@ -305,7 +305,7 @@ paths:
type: string
bufferMins:
type: string
description: ''
description: ""
/api/availability/eventtype:
post:
description: Adds a new event type for the user.
@@ -339,7 +339,7 @@ paths:
type: array
items: {}
responses:
'200':
"200":
description: OK
content:
application/json:
@@ -369,7 +369,7 @@ paths:
customInputs:
type: array
items: {}
'500':
"500":
description: Internal Server Error
patch:
description: Updates an event type for the user.
@@ -403,7 +403,7 @@ paths:
type: array
items: {}
responses:
'200':
"200":
description: OK
content:
application/json:
@@ -433,7 +433,7 @@ paths:
customInputs:
type: array
items: {}
'500':
"500":
description: Internal Server Error
delete:
description: Deletes an event type for the user.
@@ -441,16 +441,16 @@ paths:
tags:
- Availability
responses:
'200':
"200":
description: OK
content:
application/json:
schema:
type: object
properties: {}
'500':
"500":
description: Internal Server Error
'/api/book/{user}':
"/api/book/event":
post:
description: Creates a booking in the user's calendar.
summary: Creates a booking for a user
@@ -480,12 +480,19 @@ paths:
guests:
type: array
items: {}
users:
type: array
items: {}
user:
type: string
notes:
type: string
location:
type: string
paymentUid:
type: string
responses:
'204':
"204":
description: No Content
content:
application/json:
@@ -494,7 +501,7 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
content:
application/json:
@@ -528,7 +535,7 @@ paths:
confirmed:
type: string
responses:
'204':
"204":
description: No Content
content:
application/json:
@@ -537,7 +544,7 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
/api/integrations:
get:
@@ -546,12 +553,12 @@ paths:
tags:
- Integrations
responses:
'200':
"200":
description: OK
content:
application/json:
schema:
description: ''
description: ""
type: object
x-examples:
example-1:
@@ -562,7 +569,7 @@ paths:
id: 83
type: google_calendar
key:
scope: 'https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events'
scope: "https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events"
token_type: Bearer
expiry_date: 1630838974808
access_token: ya29.a0ARrdaM89R686rUyBBluTuD69oQ6WIIjjMa2xjJ0qe_5u-9ShDL09KNN1mCYoks3NP54FUMzYKmqTzb8nzCJX9jlNKP7X7-gukO4--HUyfOUbFHlHbfQ2Ei05F8AQn_xS0E_awhDgyn2anvrvEw72U3_65Zi4v6Y
@@ -660,7 +667,7 @@ paths:
- description
required:
- pageProps
'500':
"500":
description: Internal Server Error
content:
application/json:
@@ -683,7 +690,7 @@ paths:
id:
type: number
responses:
'200':
"200":
description: OK
content:
application/json:
@@ -692,7 +699,7 @@ paths:
properties:
message:
type: string
'401':
"401":
description: Unauthorized
content:
application/json:
@@ -701,7 +708,7 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
content:
application/json:
@@ -773,7 +780,7 @@ paths:
theme:
type: string
responses:
'200':
"200":
description: OK
content:
application/json:
@@ -782,7 +789,7 @@ paths:
properties:
message:
type: string
'401':
"401":
description: Unauthorized
content:
application/json:
@@ -791,7 +798,41 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
content:
application/json:
schema:
type: object
properties:
message:
type: string
/api/me:
get:
description: Gets current user's profile.
summary: Gets current user's profile.
tags:
- User
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
message:
type: string
"401":
description: Unauthorized
content:
application/json:
schema:
type: object
properties:
message:
type: string
"500":
description: Internal Server Error
content:
application/json:
@@ -812,7 +853,7 @@ paths:
schema:
type: object
responses:
'200':
"200":
description: OK
content:
application/json:
@@ -821,7 +862,7 @@ paths:
properties:
message:
type: string
'401':
"401":
description: Unauthorized
content:
application/json:
@@ -830,7 +871,7 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
content:
application/json:
@@ -865,7 +906,7 @@ paths:
properties:
teamId:
type: string
'/api/{team}':
"/api/{team}":
delete:
description: Deletes a team
summary: Deletes a team
@@ -873,9 +914,9 @@ paths:
- Teams
parameters: []
responses:
'204':
"204":
description: No Content
'401':
"401":
description: Unauthorized
content:
application/json:
@@ -884,7 +925,7 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
content:
application/json:
@@ -900,7 +941,7 @@ paths:
in: path
required: true
description: The team which you wish to modify
'/api/{team}/invite':
"/api/{team}/invite":
post:
description: Invites someone to a team.
summary: Invites someone to a team
@@ -926,7 +967,7 @@ paths:
in: path
required: true
description: The team which you wish to send the invite for
'/api/{team}/membership':
"/api/{team}/membership":
get:
description: Lists the members of a team.
summary: Lists members of a team
@@ -934,7 +975,7 @@ paths:
- Teams
parameters: []
responses:
'200':
"200":
description: OK
content:
application/json:
@@ -944,7 +985,7 @@ paths:
members:
type: array
items: {}
'401':
"401":
description: Unauthorized
content:
application/json:
@@ -953,7 +994,7 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
content:
application/json:
@@ -976,14 +1017,14 @@ paths:
userId:
type: number
responses:
'200':
"200":
description: OK
content:
application/json:
schema:
type: object
properties: {}
'401':
"401":
description: Unauthorized
content:
application/json:
@@ -992,7 +1033,7 @@ paths:
properties:
message:
type: string
'500':
"500":
description: Internal Server Error
content:
application/json:
@@ -1009,7 +1050,7 @@ paths:
required: true
description: The team which you wish to list members of
servers:
- url: 'https://app.cal.com'
- url: "https://app.cal.com"
description: Production
components:
securitySchemes: {}

View File

@@ -1,22 +0,0 @@
import { useRouter } from "next/router";
import Link from "next/link";
import React, { Children } from "react";
const ActiveLink = ({ children, activeClassName, ...props }) => {
const { asPath } = useRouter();
const child = Children.only(children);
const childClassName = child.props.className || "";
const className =
asPath === props.href || asPath === props.as
? `${childClassName} ${activeClassName}`.trim()
: childClassName;
return <Link {...props}>{React.cloneElement(child, { className })}</Link>;
};
ActiveLink.defaultProps = {
activeClassName: "active",
} as Partial<Props>;
export default ActiveLink;

View File

@@ -0,0 +1,51 @@
import { XIcon } from "@heroicons/react/outline";
import { useState } from "react";
import { useLocale } from "@lib/hooks/useLocale";
export default function AddToHomescreen() {
const { t } = useLocale();
const [closeBanner, setCloseBanner] = useState(false);
if (typeof window !== "undefined") {
if (window.matchMedia("(display-mode: standalone)").matches) {
return null;
}
}
return !closeBanner ? (
<div className="fixed sm:hidden bottom-0 inset-x-0 pb-2 sm:pb-5">
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div className="p-2 rounded-lg shadow-lg sm:p-3" style={{ background: "#2F333D" }}>
<div className="flex items-center justify-between flex-wrap">
<div className="w-0 flex-1 flex items-center">
<span className="flex p-2 rounded-lg bg-opacity-30 bg-brand">
<svg
className="h-7 w-7 text-indigo-500 fill-current"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 50 50"
enableBackground="new 0 0 50 50">
<path d="M30.3 13.7L25 8.4l-5.3 5.3-1.4-1.4L25 5.6l6.7 6.7z" />
<path d="M24 7h2v21h-2z" />
<path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z" />
</svg>
</span>
<p className="ml-3 text-xs font-medium text-white">
<span className="inline">{t("add_to_homescreen")}</span>
</p>
</div>
<div className="order-2 flex-shrink-0 sm:order-3 sm:ml-2">
<button
onClick={() => setCloseBanner(true)}
type="button"
className="-mr-1 flex p-2 rounded-md hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
<span className="sr-only">{t("dismiss")}</span>
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</div>
) : null;
}

View File

@@ -0,0 +1,30 @@
import React from "react";
import { useLocale } from "@lib/hooks/useLocale";
import NavTabs from "./NavTabs";
export default function BookingsShell({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
const tabs = [
{
name: t("upcoming"),
href: "/bookings/upcoming",
},
{
name: t("past"),
href: "/bookings/past",
},
{
name: t("cancelled"),
href: "/bookings/cancelled",
},
];
return (
<>
<NavTabs tabs={tabs} linkProps={{ shallow: true }} />
<main>{children}</main>
</>
);
}

View File

@@ -0,0 +1,9 @@
import { Suspense, SuspenseProps } from "react";
/**
* Wrapper around `<Suspense />` which will render the `fallback` when on server
* Can be simply replaced by `<Suspense />` once React 18 is ready.
*/
export const ClientSuspense = (props: SuspenseProps) => {
return <>{typeof window !== "undefined" ? <Suspense {...props} /> : props.fallback}</>;
};

View File

@@ -0,0 +1,10 @@
import { useEffect } from "react";
const BrandColor = ({ val = "#292929" }: { val: string | undefined | null }) => {
useEffect(() => {
document.documentElement.style.setProperty("--brand-color", val);
}, [val]);
return null;
};
export default BrandColor;

View File

@@ -1,7 +1,7 @@
import React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import React, { ReactNode } from "react";
type DialogProps = React.ComponentProps<typeof DialogPrimitive["Root"]>;
export type DialogProps = React.ComponentProps<typeof DialogPrimitive["Root"]>;
export function Dialog(props: DialogProps) {
const { children, ...other } = props;
return (
@@ -25,19 +25,25 @@ export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps
);
type DialogHeaderProps = {
title: React.ReactElement | string;
subtitle: React.ReactElement | string;
title: React.ReactNode;
subtitle?: React.ReactNode;
};
export function DialogHeader({ title, subtitle }: DialogHeaderProps) {
export function DialogHeader(props: DialogHeaderProps) {
return (
<div className="mb-8">
<h3 className="text-gray-900 text-lg font-bold leading-6" id="modal-title">
{title}
<h3 className="font-cal text-gray-900 text-lg font-bold leading-6" id="modal-title">
{props.title}
</h3>
<div>
<p className="text-gray-400 text-sm">{subtitle}</p>
</div>
{props.subtitle && <div className="text-gray-400 text-sm">{props.subtitle}</div>}
</div>
);
}
export function DialogFooter(props: { children: ReactNode }) {
return (
<div>
<div className="mt-5 flex space-x-2 justify-end">{props.children}</div>
</div>
);
}

View File

@@ -1,41 +0,0 @@
import { GiftIcon } from "@heroicons/react/outline";
export default function DonateBanner() {
if (location.hostname.endsWith(".cal.com")) {
return null;
}
return (
<>
<div className="h-12" />
<div className="fixed inset-x-0 bottom-0">
<div className="bg-blue-600">
<div className="max-w-7xl mx-auto py-3 px-3 sm:px-6 lg:px-8">
<div className="flex items-center justify-between flex-wrap">
<div className="w-0 flex-1 flex items-center">
<span className="flex p-2 rounded-lg bg-blue-600">
<GiftIcon className="h-6 w-6 text-white" aria-hidden="true" />
</span>
<p className="ml-3 font-medium text-white truncate">
<span className="md:hidden">Support the ongoing development</span>
<span className="hidden md:inline">
You&apos;re using the free self-hosted version. Support the ongoing development by making
a donation.
</span>
</p>
</div>
<div className="order-3 mt-2 flex-shrink-0 w-full sm:order-2 sm:mt-0 sm:w-auto">
<a
target="_blank"
rel="noreferrer"
href="https://cal.com/donate"
className="flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-blue-600 bg-white hover:bg-blue-50">
Donate
</a>
</div>
</div>
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import { SVGComponent } from "@lib/types/SVGComponent";
export default function EmptyScreen({
Icon,
headline,
description,
}: {
Icon: SVGComponent;
headline: string;
description: string;
}) {
return (
<>
<div className="min-h-80 border border-dashed rounded-sm flex justify-center items-center flex-col my-6">
<div className="bg-white w-[72px] h-[72px] flex justify-center items-center rounded-full">
<Icon className="inline-block w-10 h-10 bg-white" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="text-lg font-medium mt-6 mb-1">{headline}</h2>
<p className="text-sm leading-6 text-gray-600">{description}</p>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,28 @@
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
import { trpc } from "@lib/trpc";
export function useViewerI18n() {
return trpc.useQuery(["viewer.i18n"], {
staleTime: Infinity,
});
}
/**
* Auto-switches locale client-side to the logged in user's preference
*/
const I18nLanguageHandler = (): null => {
const { i18n } = useTranslation("common");
const locale = useViewerI18n().data?.locale;
useEffect(() => {
if (locale && i18n.language && i18n.language !== locale) {
if (typeof i18n.changeLanguage === "function") i18n.changeLanguage(locale);
}
}, [locale, i18n]);
return null;
};
export default I18nLanguageHandler;

View File

@@ -1,215 +1,164 @@
import { FormEvent, useCallback, useEffect, useState } from "react";
import Cropper from "react-easy-crop";
import { useCallback, useRef, useState } from "react";
import Slider from "./Slider";
export default function ImageUploader({ target, id, buttonMsg, handleAvatarChange, imageRef }) {
const imageFileRef = useRef<HTMLInputElement>();
const [imageDataUrl, setImageDataUrl] = useState<string>();
const [croppedAreaPixels, setCroppedAreaPixels] = useState();
const [rotation] = useState(1);
import { Area, getCroppedImg } from "@lib/cropImage";
import { useFileReader } from "@lib/hooks/useFileReader";
import { useLocale } from "@lib/hooks/useLocale";
import { DialogClose, DialogTrigger, Dialog, DialogContent } from "@components/Dialog";
import Slider from "@components/Slider";
import Button from "@components/ui/Button";
type ImageUploaderProps = {
id: string;
buttonMsg: string;
handleAvatarChange: (imageSrc: string) => void;
imageSrc?: string;
target: string;
};
interface FileEvent<T = Element> extends FormEvent<T> {
target: EventTarget & T;
}
// This is separate to prevent loading the component until file upload
function CropContainer({
onCropComplete,
imageSrc,
}: {
imageSrc: string;
onCropComplete: (croppedAreaPixels: Area) => void;
}) {
const { t } = useLocale();
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [imageLoaded, setImageLoaded] = useState(false);
const [isImageShown, setIsImageShown] = useState(false);
const [shownImage, setShownImage] = useState<string>();
const [imageUploadModalOpen, setImageUploadModalOpen] = useState(false);
const openUploaderModal = () => {
imageRef ? (setIsImageShown(true), setShownImage(imageRef)) : setIsImageShown(false);
setImageUploadModalOpen(!imageUploadModalOpen);
};
const closeImageUploadModal = () => {
setImageUploadModalOpen(false);
};
async function ImageUploadHandler() {
const img = await readFile(imageFileRef.current.files[0]);
setImageDataUrl(img);
CropHandler();
}
const readFile = (file) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.addEventListener("load", () => resolve(reader.result), false);
reader.readAsDataURL(file);
});
};
const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => {
setCroppedAreaPixels(croppedAreaPixels);
}, []);
const CropHandler = () => {
setCrop({ x: 0, y: 0 });
setZoom(1);
setImageLoaded(true);
};
const handleZoomSliderChange = ([value]) => {
const handleZoomSliderChange = (value: number) => {
value < 1 ? setZoom(1) : setZoom(value);
};
const createImage = (url) =>
new Promise<HTMLImageElement>((resolve, reject) => {
const image = new Image();
image.addEventListener("load", () => resolve(image));
image.addEventListener("error", (error) => reject(error));
image.setAttribute("crossOrigin", "anonymous"); // needed to avoid cross-origin issues on CodeSandbox
image.src = url;
});
function getRadianAngle(degreeValue) {
return (degreeValue * Math.PI) / 180;
}
async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) {
const image = await createImage(imageSrc);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const maxSize = Math.max(image.width, image.height);
const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2));
// set each dimensions to double largest dimension to allow for a safe area for the
// image to rotate in without being clipped by canvas context
canvas.width = safeArea;
canvas.height = safeArea;
// translate canvas context to a central location on image to allow rotating around the center.
ctx.translate(safeArea / 2, safeArea / 2);
ctx.rotate(getRadianAngle(rotation));
ctx.translate(-safeArea / 2, -safeArea / 2);
// draw rotated image and store data.
ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5);
const data = ctx.getImageData(0, 0, safeArea, safeArea);
// set canvas width to final desired crop size - this will clear existing context
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
// paste generated rotate image with correct offsets for x,y crop values.
ctx.putImageData(
data,
Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x),
Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y)
);
// As Base64 string
return canvas.toDataURL("image/jpeg");
}
const showCroppedImage = useCallback(async () => {
try {
const croppedImage = await getCroppedImg(imageDataUrl, croppedAreaPixels, rotation);
setIsImageShown(true);
setShownImage(croppedImage);
setImageLoaded(false);
handleAvatarChange(croppedImage);
closeImageUploadModal();
} catch (e) {
console.error(e);
}
}, [croppedAreaPixels, rotation]);
return (
<div className="flex justify-center items-center">
<button
type="button"
className="ml-4 cursor-pointer inline-flex items-center px-4 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;"
onClick={openUploaderModal}>
{buttonMsg}
</button>
{imageUploadModalOpen && (
<div
className="fixed z-10 inset-0 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"></div>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<div className="inline-block align-bottom bg-white rounded-sm px-4 pt-5 pb-4 text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-md sm:w-full sm:p-6">
<div className="sm:flex sm:items-start mb-4">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<h3 className="text-lg leading-6 font-bold text-gray-900" id="modal-title">
Upload {target}
</h3>
</div>
</div>
<div className="mb-4">
<div className="cropper mt-6 flex flex-col justify-center items-center p-8 bg-gray-50">
{!imageLoaded && (
<div className="flex justify-start items-center bg-gray-500 max-h-20 h-20 w-20 rounded-full">
{!isImageShown && (
<p className="sm:text-xs text-sm text-white w-full text-center">No {target}</p>
)}
{isImageShown && (
<img className="h-20 w-20 rounded-full" src={shownImage} alt={target} />
)}
</div>
)}
{imageLoaded && (
<div className="crop-container max-h-40 h-40 w-40 rounded-full">
<div className="relative h-40 w-40 rounded-full">
<Cropper
image={imageDataUrl}
crop={crop}
zoom={zoom}
aspect={1 / 1}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
</div>
<Slider
value={zoom}
min={1}
max={3}
step={0.1}
label="Slide to zoom, drag to reposition"
changeHandler={handleZoomSliderChange}
/>
</div>
)}
<label
htmlFor={id}
className="mt-8 cursor-pointer inline-flex items-center px-4 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-500;">
Choose a file...
</label>
<input
onChange={ImageUploadHandler}
ref={imageFileRef}
type="file"
id={id}
name={id}
placeholder="Upload image"
className="mt-4 pointer-events-none opacity-0 absolute"
accept="image/*"
/>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="button" className="btn btn-primary" onClick={showCroppedImage}>
Save
</button>
<button onClick={closeImageUploadModal} type="button" className="btn btn-white mr-2">
Cancel
</button>
</div>
</div>
</div>
</div>
)}
<div className="w-40 h-40 rounded-full crop-container max-h-40">
<div className="relative w-40 h-40 rounded-full">
<Cropper
image={imageSrc}
crop={crop}
zoom={zoom}
aspect={1}
onCropChange={setCrop}
onCropComplete={(croppedArea, croppedAreaPixels) => onCropComplete(croppedAreaPixels)}
onZoomChange={setZoom}
/>
</div>
<Slider
value={zoom}
min={1}
max={3}
step={0.1}
label={t("slide_zoom_drag_instructions")}
changeHandler={handleZoomSliderChange}
/>
</div>
);
}
export default function ImageUploader({
target,
id,
buttonMsg,
handleAvatarChange,
...props
}: ImageUploaderProps) {
const { t } = useLocale();
const [imageSrc, setImageSrc] = useState<string | null>();
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>();
const [{ result }, setFile] = useFileReader({
method: "readAsDataURL",
});
useEffect(() => {
setImageSrc(props.imageSrc);
}, [props.imageSrc]);
const onInputFile = (e: FileEvent<HTMLInputElement>) => {
if (!e.target.files?.length) {
return;
}
setFile(e.target.files[0]);
};
const showCroppedImage = useCallback(
async (croppedAreaPixels) => {
try {
const croppedImage = await getCroppedImg(
result as string /* result is always string when using readAsDataUrl */,
croppedAreaPixels
);
setImageSrc(croppedImage);
handleAvatarChange(croppedImage);
} catch (e) {
console.error(e);
}
},
[result, handleAvatarChange]
);
return (
<Dialog
onOpenChange={
(opened) => !opened && setFile(null) // unset file on close
}>
<DialogTrigger asChild>
<div className="flex items-center px-3">
<Button color="secondary" type="button" className="py-1 text-xs">
{buttonMsg}
</Button>
</div>
</DialogTrigger>
<DialogContent>
<div className="mb-4 sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:text-left">
<h3 className="text-lg font-bold leading-6 text-gray-900 font-cal" id="modal-title">
{t("upload_target", { target })}
</h3>
</div>
</div>
<div className="mb-4">
<div className="flex flex-col items-center justify-center p-8 mt-6 cropper bg-gray-50">
{!result && (
<div className="flex items-center justify-start w-20 h-20 bg-gray-500 rounded-full max-h-20">
{!imageSrc && (
<p className="w-full text-sm text-center text-white sm:text-xs">
{t("no_target", { target })}
</p>
)}
{imageSrc && <img className="w-20 h-20 rounded-full" src={imageSrc} alt={target} />}
</div>
)}
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
<label className="px-3 py-1 mt-8 text-xs font-medium leading-4 text-gray-700 bg-white border border-gray-300 rounded-sm hover:bg-gray-50 hover:text-gray-900 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-neutral-900 dark:bg-transparent dark:text-white dark:border-gray-800 dark:hover:bg-gray-900">
<input
onInput={onInputFile}
type="file"
name={id}
placeholder={t("upload_image")}
className="absolute mt-4 opacity-0 pointer-events-none"
accept="image/*"
/>
{t("choose_a_file")}
</label>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-x-2">
<DialogClose asChild>
<Button onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</Button>
</DialogClose>
<DialogClose asChild>
<Button color="secondary">{t("cancel")}</Button>
</DialogClose>
</div>
</DialogContent>
</Dialog>
);
}

72
components/List.tsx Normal file
View File

@@ -0,0 +1,72 @@
import Link from "next/link";
import { createElement } from "react";
import classNames from "@lib/classNames";
export function List(props: JSX.IntrinsicElements["ul"]) {
return (
<ul {...props} className={classNames("sm:overflow-hidden rounded-sm sm:mx-0 -mx-4", props.className)}>
{props.children}
</ul>
);
}
export type ListItemProps = { expanded?: boolean } & ({ href?: never } & JSX.IntrinsicElements["li"]);
export function ListItem(props: ListItemProps) {
const { href, expanded, ...passThroughProps } = props;
const elementType = href ? "a" : "li";
const element = createElement(
elementType,
{
...passThroughProps,
className: classNames(
"items-center bg-white min-w-0 flex-1 flex border-gray-200",
expanded ? "my-2 border" : "border -mb-px last:mb-0",
props.className,
(props.onClick || href) && "hover:bg-neutral-50"
),
},
props.children
);
return href ? (
<Link passHref href={href}>
{element}
</Link>
) : (
element
);
}
export function ListItemTitle<TComponent extends keyof JSX.IntrinsicElements = "span">(
props: JSX.IntrinsicElements[TComponent] & { component?: TComponent }
) {
const { component = "span", ...passThroughProps } = props;
return createElement(
component,
{
...passThroughProps,
className: classNames("text-sm font-medium text-neutral-900 truncate", props.className),
},
props.children
);
}
export function ListItemText<TComponent extends keyof JSX.IntrinsicElements = "span">(
props: JSX.IntrinsicElements[TComponent] & { component?: TComponent }
) {
const { component = "span", ...passThroughProps } = props;
return createElement(
component,
{
...passThroughProps,
className: classNames("text-sm text-gray-500 truncate", props.className),
},
props.children
);
}

View File

@@ -1,7 +1,7 @@
export default function Loader() {
return (
<div className="loader border-black dark:border-white">
<span className="loader-inner bg-black dark:bg-white"></span>
<div className="loader border-brand dark:border-white">
<span className="loader-inner bg-brand dark:bg-white"></span>
</div>
);
}

View File

@@ -1,82 +0,0 @@
/* legacy and soon deprecated, please refactor to use <Dialog> only */
import { Fragment, ReactNode } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { CheckIcon, InformationCircleIcon } from "@heroicons/react/outline";
import classNames from "@lib/classNames";
export default function Modal(props: {
heading: ReactNode;
description: ReactNode;
handleClose: () => void;
open: boolean;
variant?: "success" | "warning";
}) {
const { variant = "success" } = props;
return (
<Transition.Root show={props.open} as={Fragment}>
<Dialog
as="div"
static
className="fixed z-50 inset-0 overflow-y-auto"
open={props.open}
onClose={props.handleClose}>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<Dialog.Overlay className="fixed inset-0 bg-gray-500 z-0 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
<div>
<div
className={classNames(
"mx-auto flex items-center justify-center h-12 w-12 rounded-full",
variant === "success" && "bg-green-100",
variant === "warning" && "bg-yellow-100"
)}>
{variant === "success" && (
<CheckIcon className="h-6 w-6 text-green-600" aria-hidden="true" />
)}
{variant === "warning" && (
<InformationCircleIcon className={"h-6 w-6 text-yellow-400"} aria-hidden="true" />
)}
</div>
<div className="mt-3 text-center sm:mt-5">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
{props.heading}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">{props.description}</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-6">
<button type="button" className="btn-wide btn-primary" onClick={() => props.handleClose()}>
Dismiss
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
);
}

53
components/NavTabs.tsx Normal file
View File

@@ -0,0 +1,53 @@
import Link, { LinkProps } from "next/link";
import { useRouter } from "next/router";
import React, { ElementType, FC } from "react";
import classNames from "@lib/classNames";
interface Props {
tabs: {
name: string;
href: string;
icon?: ElementType;
}[];
linkProps?: Omit<LinkProps, "href">;
}
const NavTabs: FC<Props> = ({ tabs, linkProps }) => {
const router = useRouter();
return (
<>
<nav className="-mb-px flex space-x-2 sm:space-x-5" aria-label="Tabs">
{tabs.map((tab) => {
const isCurrent = router.asPath === tab.href;
return (
<Link {...linkProps} key={tab.name} href={tab.href}>
<a
className={classNames(
isCurrent
? "border-neutral-900 text-neutral-900"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
"group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm"
)}
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 mr-2 h-5 w-5 hidden sm:inline-block"
)}
aria-hidden="true"
/>
)}
<span>{tab.name}</span>
</a>
</Link>
);
})}
</nav>
<hr />
</>
);
};
export default NavTabs;

View File

@@ -1,68 +0,0 @@
import Link from "next/link";
import { CodeIcon, CreditCardIcon, KeyIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import classNames from "@lib/classNames";
export default function SettingsShell(props) {
const router = useRouter();
const tabs = [
{
name: "Profile",
href: "/settings/profile",
icon: UserIcon,
current: router.pathname == "/settings/profile",
},
{
name: "Security",
href: "/settings/security",
icon: KeyIcon,
current: router.pathname == "/settings/security",
},
{ name: "Embed", href: "/settings/embed", icon: CodeIcon, current: router.pathname == "/settings/embed" },
{
name: "Teams",
href: "/settings/teams",
icon: UserGroupIcon,
current: router.pathname == "/settings/teams",
},
{
name: "Billing",
href: "/settings/billing",
icon: CreditCardIcon,
current: router.pathname == "/settings/billing",
},
];
return (
<div>
<div className="sm:mx-auto">
<nav className="-mb-px flex space-x-2 sm:space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<Link key={tab.name} href={tab.href}>
<a
className={classNames(
tab.current
? "border-neutral-900 text-neutral-900"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300",
"group inline-flex items-center py-4 px-1 border-b-2 font-medium text-sm"
)}
aria-current={tab.current ? "page" : undefined}>
<tab.icon
className={classNames(
tab.current ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
"-ml-0.5 mr-2 h-5 w-5 hidden sm:inline-block"
)}
aria-hidden="true"
/>
<span>{tab.name}</span>
</a>
</Link>
))}
</nav>
<hr />
</div>
<main className="max-w-4xl">{props.children}</main>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { CreditCardIcon, KeyIcon, UserGroupIcon, UserIcon } from "@heroicons/react/solid";
import React from "react";
import { useLocale } from "@lib/hooks/useLocale";
import NavTabs from "./NavTabs";
export default function SettingsShell({ children }: { children: React.ReactNode }) {
const { t } = useLocale();
const tabs = [
{
name: t("profile"),
href: "/settings/profile",
icon: UserIcon,
},
{
name: t("security"),
href: "/settings/security",
icon: KeyIcon,
},
{
name: t("teams"),
href: "/settings/teams",
icon: UserGroupIcon,
},
{
name: t("billing"),
href: "/settings/billing",
icon: CreditCardIcon,
},
];
return (
<>
<div className="sm:mx-auto">
<NavTabs tabs={tabs} />
</div>
<main className="max-w-4xl">{children}</main>
</>
);
}

View File

@@ -1,14 +1,6 @@
import Link from "next/link";
import React, { Fragment, useEffect, useState } from "react";
import { useRouter } from "next/router";
import { signOut, useSession } from "next-auth/client";
// TODO: replace headlessui with radix-ui
import { Menu, Transition } from "@headlessui/react";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { SelectorIcon } from "@heroicons/react/outline";
import {
CalendarIcon,
ChatAltIcon,
ClockIcon,
CogIcon,
ExternalLinkIcon,
@@ -16,69 +8,184 @@ import {
LogoutIcon,
PuzzleIcon,
} from "@heroicons/react/solid";
import Logo from "./Logo";
import classNames from "@lib/classNames";
import { signOut, useSession } from "next-auth/client";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { ReactNode, useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import Avatar from "@components/ui/Avatar";
import { User } from "@prisma/client";
import { HeadSeo } from "@components/seo/head-seo";
export default function Shell(props) {
const router = useRouter();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import LicenseBanner from "@ee/components/LicenseBanner";
import HelpMenuItemDynamic from "@ee/lib/intercom/HelpMenuItemDynamic";
import classNames from "@lib/classNames";
import { shouldShowOnboarding } from "@lib/getting-started";
import { useLocale } from "@lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";
import Avatar from "@components/ui/Avatar";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@components/ui/Dropdown";
import { useViewerI18n } from "./I18nLanguageHandler";
import Logo from "./Logo";
function useMeQuery() {
const meQuery = trpc.useQuery(["viewer.me"], {
retry(failureCount) {
return failureCount > 3;
},
});
return meQuery;
}
function useRedirectToLoginIfUnauthenticated() {
const [session, loading] = useSession();
const router = useRouter();
useEffect(() => {
if (!loading && !session) {
router.replace({
pathname: "/auth/login",
query: {
callbackUrl: `${location.pathname}${location.search}`,
},
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loading, session]);
return {
loading: loading && !session,
};
}
function useRedirectToOnboardingIfNeeded() {
const router = useRouter();
const query = useMeQuery();
const user = query.data;
const [isRedirectingToOnboarding, setRedirecting] = useState(false);
useEffect(() => {
if (user && shouldShowOnboarding(user)) {
setRedirecting(true);
}
}, [router, user]);
useEffect(() => {
if (isRedirectingToOnboarding) {
router.replace({
pathname: "/getting-started",
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRedirectingToOnboarding]);
return {
isRedirectingToOnboarding,
};
}
export function ShellSubHeading(props: {
title: ReactNode;
subtitle?: ReactNode;
actions?: ReactNode;
className?: string;
}) {
return (
<div className={classNames("block sm:flex justify-between mb-3", props.className)}>
<div>
<h2 className="flex items-center content-center space-x-2 text-base font-bold leading-6 text-gray-900">
{props.title}
</h2>
{props.subtitle && <p className="mr-4 text-sm text-neutral-500">{props.subtitle}</p>}
</div>
{props.actions && <div className="flex-shrink-0 mb-4">{props.actions}</div>}
</div>
);
}
export default function Shell(props: {
centered?: boolean;
title?: string;
heading: ReactNode;
subtitle?: ReactNode;
children: ReactNode;
CTA?: ReactNode;
}) {
const { t } = useLocale();
const router = useRouter();
const { loading } = useRedirectToLoginIfUnauthenticated();
const { isRedirectingToOnboarding } = useRedirectToOnboardingIfNeeded();
const telemetry = useTelemetry();
const navigation = [
{
name: "Event Types",
name: t("event_types_page_title"),
href: "/event-types",
icon: LinkIcon,
current: router.pathname.startsWith("/event-types"),
current: router.asPath.startsWith("/event-types"),
},
{
name: "Bookings",
href: "/bookings",
name: t("bookings"),
href: "/bookings/upcoming",
icon: ClockIcon,
current: router.pathname.startsWith("/bookings"),
current: router.asPath.startsWith("/bookings"),
},
{
name: "Availability",
name: t("availability"),
href: "/availability",
icon: CalendarIcon,
current: router.pathname.startsWith("/availability"),
current: router.asPath.startsWith("/availability"),
},
{
name: "Integrations",
name: t("integrations"),
href: "/integrations",
icon: PuzzleIcon,
current: router.pathname.startsWith("/integrations"),
current: router.asPath.startsWith("/integrations"),
},
{
name: "Settings",
name: t("settings"),
href: "/settings/profile",
icon: CogIcon,
current: router.pathname.startsWith("/settings"),
current: router.asPath.startsWith("/settings"),
},
];
useEffect(() => {
telemetry.withJitsu((jitsu) => {
return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.pathname));
return jitsu.track(telemetryEventTypes.pageView, collectPageParameters(router.asPath));
});
}, [telemetry]);
if (!loading && !session) {
router.replace("/auth/login");
}
}, [telemetry, router.asPath]);
const pageTitle = typeof props.heading === "string" ? props.heading : props.title;
return session ? (
const query = useMeQuery();
const user = query.data;
const i18n = useViewerI18n();
if (i18n.status === "loading" || isRedirectingToOnboarding || loading) {
// show spinner whilst i18n is loading to avoid language flicker
return (
<div className="absolute z-50 flex items-center w-full h-screen bg-gray-50">
<Loader />
</div>
);
}
return (
<>
<CustomBranding val={user?.brandColor} />
<HeadSeo
title={pageTitle}
description={props.subtitle}
title={pageTitle ?? "Cal.com"}
description={props.subtitle ? props.subtitle?.toString() : ""}
nextSeoProps={{
nofollow: true,
noindex: true,
@@ -88,19 +195,17 @@ export default function Shell(props) {
<Toaster position="bottom-right" />
</div>
<div className="h-screen flex overflow-hidden bg-gray-100">
{/* Static sidebar for desktop */}
<div className="flex h-screen overflow-hidden bg-gray-100">
<div className="hidden md:flex md:flex-shrink-0">
<div className="flex flex-col w-56">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex flex-col h-0 flex-1 border-r border-gray-200 bg-white">
<div className="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
<div className="flex flex-col flex-1 h-0 bg-white border-r border-gray-200">
<div className="flex flex-col flex-1 pt-5 pb-4 overflow-y-auto">
<Link href="/event-types">
<a className="px-4">
<Logo small />
</a>
</Link>
<nav className="mt-5 flex-1 px-2 bg-white space-y-1">
<nav className="flex-1 px-2 mt-5 space-y-1 bg-white">
{navigation.map((item) => (
<Link key={item.name} href={item.href}>
<a
@@ -125,51 +230,50 @@ export default function Shell(props) {
))}
</nav>
</div>
<div className="flex-shrink-0 flex p-4">
<div className="p-2 pt-2 pr-2 m-2 rounded-sm hover:bg-gray-100">
<UserDropdown />
</div>
</div>
</div>
</div>
<div className="flex flex-col w-0 flex-1 overflow-hidden">
<main className="flex-1 relative z-0 overflow-y-auto focus:outline-none">
<div className="flex flex-col flex-1 w-0 overflow-hidden">
<main className="flex-1 relative z-0 overflow-y-auto focus:outline-none max-w-[1700px]">
{/* show top navigation for md and smaller (tablet and phones) */}
<nav className="md:hidden bg-white shadow p-4 flex justify-between items-center">
<nav className="flex items-center justify-between p-4 bg-white border-b border-gray-200 md:hidden">
<Link href="/event-types">
<a>
<Logo />
</a>
</Link>
<div className="flex gap-3 items-center self-center">
<button className="bg-white p-2 rounded-full text-gray-400 hover:text-gray-500 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span className="sr-only">View notifications</span>
<div className="flex items-center self-center gap-3">
<button className="p-2 text-gray-400 bg-white rounded-full hover:text-gray-500 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
<span className="sr-only">{t("view_notifications")}</span>
<Link href="/settings/profile">
<a>
<CogIcon className="h-6 w-6" aria-hidden="true" />
<CogIcon className="w-6 h-6" aria-hidden="true" />
</a>
</Link>
</button>
<div className="mt-1">
<UserDropdown small bottom session={session} />
</div>
<UserDropdown small />
</div>
</nav>
<div className="py-8">
<div className="block sm:flex justify-between px-4 sm:px-6 md:px-8">
<div className="mb-8">
<h1 className="text-xl font-bold text-gray-900">{props.heading}</h1>
<p className="text-sm text-neutral-500 mr-4">{props.subtitle}</p>
<div className={classNames(props.centered && "md:max-w-5xl mx-auto", "py-8")}>
<div className="block sm:flex justify-between px-4 sm:px-6 md:px-8 min-h-[80px]">
<div className="w-full mb-8">
<h1 className="mb-1 text-xl font-bold tracking-wide text-gray-900 font-cal">
{props.heading}
</h1>
<p className="mr-4 text-sm text-neutral-500">{props.subtitle}</p>
</div>
<div className="mb-4 flex-shrink-0">{props.CTA}</div>
<div className="flex-shrink-0 mb-4">{props.CTA}</div>
</div>
<div className="px-4 sm:px-6 md:px-8">{props.children}</div>
{/* show bottom navigation for md and smaller (tablet and phones) */}
<nav className="bottom-nav md:hidden flex fixed bottom-0 bg-white w-full shadow">
<nav className="fixed bottom-0 flex w-full bg-white shadow bottom-nav md:hidden">
{/* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */}
{navigation.flatMap((item, itemIdx) =>
item.name === "Settings" ? (
item.href === "/settings/profile" ? (
[]
) : (
<Link key={item.name} href={item.href}>
@@ -188,172 +292,113 @@ export default function Shell(props) {
)}
aria-hidden="true"
/>
<span>{item.name}</span>
<span className="truncate">{item.name}</span>
</a>
</Link>
)
)}
</nav>
{/* add padding to content for mobile navigation*/}
<div className="block md:hidden pt-12" />
<div className="block pt-12 md:hidden" />
</div>
<LicenseBanner />
</main>
</div>
</div>
</>
) : null;
}
function UserDropdown({ small, bottom }: { small?: boolean; bottom?: boolean }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch("/api/me")
.then((res) => res.json())
.then((responseBody) => {
setUser(responseBody.user);
});
}, []);
return (
<Menu as="div" className="w-full relative inline-block text-left">
{({ open }) => (
<>
<div>
{user && (
<Menu.Button className="group w-full rounded-md text-sm text-left font-medium text-gray-700 focus:outline-none">
<span className="flex w-full justify-between items-center">
<span className="flex min-w-0 items-center justify-between space-x-3">
<Avatar
imageSrc={user?.avatar}
displayName={user?.name}
className={classNames(
small ? "w-8 h-8" : "w-10 h-10",
"bg-gray-300 rounded-full flex-shrink-0"
)}
/>
{!small && (
<span className="flex-1 flex flex-col min-w-0">
<span className="text-gray-900 text-sm font-medium truncate">{user?.name}</span>
<span className="text-neutral-500 font-normal text-sm truncate">
/{user?.username}
</span>
</span>
)}
</span>
{!small && (
<SelectorIcon
className="flex-shrink-0 h-5 w-5 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
)}
</span>
</Menu.Button>
)}
</div>
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95">
<Menu.Items
static
className={classNames(
bottom ? "origin-top top-1 right-0" : "origin-bottom bottom-14 left-0",
"w-64 z-10 absolute mt-1 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 divide-y divide-gray-200 focus:outline-none"
)}>
<div className="py-1">
<a href={"/" + user?.username} className="flex px-4 py-2 text-sm text-neutral-500">
View public page <ExternalLinkIcon className="ml-1 mt-1 w-3 h-3 text-neutral-400" />
</a>
</div>
<div className="py-1">
<Menu.Item>
{({ active }) => (
<a
href="https://cal.com/slack"
target="_blank"
rel="noreferrer"
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-neutral-700",
"flex px-4 py-2 text-sm font-medium"
)}>
<svg
viewBox="0 0 2447.6 2452.5"
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"mt-0.5 mr-3 flex-shrink-0 h-4 w-4"
)}
xmlns="http://www.w3.org/2000/svg">
<g clipRule="evenodd" fillRule="evenodd">
<path
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
fill="#9BA6B6"></path>
<path
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
fill="#9BA6B6"></path>
<path
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
fill="#9BA6B6"></path>
<path
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
fill="#9BA6B6"></path>
</g>
</svg>
Join our Slack
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
href="mailto:feedback@cal.com"
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-neutral-700",
"flex px-4 py-2 text-sm font-medium"
)}>
<ChatAltIcon
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"mr-2 flex-shrink-0 h-5 w-5"
)}
aria-hidden="true"
/>
Feedback
</a>
)}
</Menu.Item>
</div>
<div className="py-1">
<Menu.Item>
{({ active }) => (
<a
onClick={() => signOut({ callbackUrl: "/auth/logout" })}
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
"flex px-4 py-2 text-sm font-medium"
)}>
<LogoutIcon
className={classNames(
"text-neutral-400 group-hover:text-neutral-500",
"mr-2 flex-shrink-0 h-5 w-5"
)}
aria-hidden="true"
/>
Sign out
</a>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
);
}
function UserDropdown({ small }: { small?: boolean }) {
const { t } = useLocale();
const query = useMeQuery();
const user = query.data;
return (
<Dropdown>
<DropdownMenuTrigger asChild>
<div className="flex items-center space-x-2 cursor-pointer group">
<Avatar
imageSrc={user?.avatar || ""}
alt={user?.username || "Nameless User"}
className={classNames(small ? "w-8 h-8" : "w-10 h-10", "bg-gray-300 rounded-full flex-shrink-0")}
/>
{!small && (
<>
<span className="flex-grow text-sm">
<span className="block font-medium text-gray-900 truncate">
{user?.username || "Nameless User"}
</span>
<span className="block font-normal truncate text-neutral-500">
{user?.username ? `cal.com/${user.username}` : "No public page"}
</span>
</span>
<SelectorIcon
className="flex-shrink-0 w-5 h-5 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent>
{user?.username && (
<DropdownMenuItem>
<a
target="_blank"
rel="noopener noreferrer"
href={`${process.env.NEXT_PUBLIC_APP_URL}/${user.username}`}
className="flex items-center px-4 py-2 text-sm text-gray-700">
<ExternalLinkIcon className="w-5 h-5 mr-3 text-gray-500" /> {t("view_public_page")}
</a>
</DropdownMenuItem>
)}
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<a
href="https://cal.com/slack"
target="_blank"
rel="noreferrer"
className="flex px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900">
<svg
viewBox="0 0 2447.6 2452.5"
className={classNames(
"text-gray-500 group-hover:text-gray-700",
"mt-0.5 mr-3 flex-shrink-0 h-4 w-4"
)}
xmlns="http://www.w3.org/2000/svg">
<g clipRule="evenodd" fillRule="evenodd">
<path
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
fill="#9BA6B6"></path>
<path
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
fill="#9BA6B6"></path>
<path
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
fill="#9BA6B6"></path>
<path
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
fill="#9BA6B6"></path>
</g>
</svg>
{t("join_our_slack")}
</a>
</DropdownMenuItem>
<HelpMenuItemDynamic />
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<a
onClick={() => signOut({ callbackUrl: "/auth/logout" })}
className="flex px-4 py-2 text-sm cursor-pointer hover:bg-gray-100 hover:text-gray-900">
<LogoutIcon
className={classNames("text-gray-500 group-hover:text-gray-700", "mr-2 flex-shrink-0 h-5 w-5")}
aria-hidden="true"
/>
{t("sign_out")}
</a>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
);
}

View File

@@ -1,15 +1,22 @@
import React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import React from "react";
const Slider = ({ value, min, max, step, label, changeHandler }) => (
const Slider = ({
value,
label,
changeHandler,
...props
}: Omit<SliderPrimitive.SliderProps, "value"> & {
value: number;
label: string;
changeHandler: (value: number) => void;
}) => (
<SliderPrimitive.Root
className="slider mt-2"
min={min}
step={step}
max={max}
className="mt-2 slider"
value={[value]}
aria-label={label}
onValueChange={changeHandler}>
onValueChange={(value: number[]) => changeHandler(value[0] ?? value)}
{...props}>
<SliderPrimitive.Track className="slider-track">
<SliderPrimitive.Range className="slider-range" />
</SliderPrimitive.Track>

View File

@@ -1,5 +1,5 @@
import React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import React from "react";
export function Tooltip({
children,
@@ -9,12 +9,11 @@ export function Tooltip({
onOpenChange,
...props
}: {
[x: string]: any;
children: React.ReactNode;
content: React.ReactNode;
open: boolean;
defaultOpen: boolean;
onOpenChange: (open: boolean) => void;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
return (
<TooltipPrimitive.Root
@@ -24,7 +23,7 @@ export function Tooltip({
onOpenChange={onOpenChange}>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<TooltipPrimitive.Content
className="bg-black text-xs -mt-2 text-white px-1 py-0.5 shadow-lg rounded-sm"
className="bg-brand text-xs -mt-2 text-white px-1 py-0.5 shadow-lg rounded-sm"
side="top"
align="center"
{...props}>

View File

@@ -1,12 +1,33 @@
import { ExclamationIcon } from "@heroicons/react/solid";
import { SchedulingType } from "@prisma/client";
import { Dayjs } from "dayjs";
import Link from "next/link";
import { useRouter } from "next/router";
import { useSlots } from "@lib/hooks/useSlots";
import { ExclamationIcon } from "@heroicons/react/solid";
import React from "react";
import Loader from "@components/Loader";
import { SchedulingType } from "@prisma/client";
import React, { FC } from "react";
const AvailableTimes = ({
import { useLocale } from "@lib/hooks/useLocale";
import { useSlots } from "@lib/hooks/useSlots";
import Loader from "@components/Loader";
type AvailableTimesProps = {
workingHours: {
days: number[];
startTime: number;
endTime: number;
}[];
timeFormat: string;
minimumBookingNotice: number;
eventTypeId: number;
eventLength: number;
date: Dayjs;
users: {
username: string | null;
}[];
schedulingType: SchedulingType | null;
};
const AvailableTimes: FC<AvailableTimesProps> = ({
date,
eventLength,
eventTypeId,
@@ -16,6 +37,7 @@ const AvailableTimes = ({
users,
schedulingType,
}) => {
const { t } = useLocale();
const router = useRouter();
const { rescheduleUid } = router.query;
@@ -26,66 +48,78 @@ const AvailableTimes = ({
workingHours,
users,
minimumBookingNotice,
eventTypeId,
});
return (
<div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:max-h-97 overflow-y-auto">
<div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:-mb-5">
<div className="text-gray-600 font-light text-lg mb-4 text-left">
<span className="w-1/2 dark:text-white text-gray-600">
<strong>{date.format("dddd")}</strong>
<span className="text-gray-500">{date.format(", DD MMMM")}</span>
<strong>{t(date.format("dddd").toLowerCase())}</strong>
<span className="text-gray-500">
{date.format(", DD ")}
{t(date.format("MMMM").toLowerCase())}
</span>
</span>
</div>
{!loading &&
slots?.length > 0 &&
slots.map((slot) => {
const bookingUrl = {
pathname: "book",
query: {
...router.query,
date: slot.time.format(),
type: eventTypeId,
},
};
<div className="md:max-h-[364px] overflow-y-auto">
{!loading &&
slots?.length > 0 &&
slots.map((slot) => {
type BookingURL = {
pathname: string;
query: Record<string, string | number | string[] | undefined>;
};
const bookingUrl: BookingURL = {
pathname: "book",
query: {
...router.query,
date: slot.time.format(),
type: eventTypeId,
},
};
if (rescheduleUid) {
bookingUrl.query.rescheduleUid = rescheduleUid;
}
if (rescheduleUid) {
bookingUrl.query.rescheduleUid = rescheduleUid as string;
}
if (schedulingType === SchedulingType.ROUND_ROBIN) {
bookingUrl.query.user = slot.users;
}
if (schedulingType === SchedulingType.ROUND_ROBIN) {
bookingUrl.query.user = slot.users;
}
return (
<div key={slot.time.format()}>
<Link href={bookingUrl}>
<a className="block font-medium mb-4 bg-white dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border border-primary-500 dark:border-transparent rounded-sm hover:text-white hover:bg-primary-500 dark:hover:border-black py-4 dark:hover:bg-black">
{slot.time.format(timeFormat)}
</a>
</Link>
</div>
);
})}
{!loading && !error && !slots.length && (
<div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4">
<h1 className="text-xl text-black dark:text-white">All booked today.</h1>
</div>
)}
return (
<div key={slot.time.format()}>
<Link href={bookingUrl}>
<a
className="block font-medium mb-2 bg-white dark:bg-gray-600 text-primary-500 dark:text-neutral-200 border border-brand dark:border-transparent rounded-sm hover:text-white hover:bg-brand dark:hover:border-black py-4 dark:hover:bg-black"
data-testid="time">
{slot.time.format(timeFormat)}
</a>
</Link>
</div>
);
})}
{!loading && !error && !slots.length && (
<div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4">
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
</div>
)}
{loading && <Loader />}
{loading && <Loader />}
{error && (
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">Could not load the available time slots.</p>
{error && (
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">{t("slots_load_fail")}</p>
</div>
</div>
</div>
</div>
)}
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,133 @@
import { BanIcon, CheckIcon, ClockIcon, XIcon } from "@heroicons/react/outline";
import { BookingStatus } from "@prisma/client";
import dayjs from "dayjs";
import { useMutation } from "react-query";
import { HttpError } from "@lib/core/http/error";
import { useLocale } from "@lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@lib/trpc";
import TableActions, { ActionType } from "@components/ui/TableActions";
type BookingItem = inferQueryOutput<"viewer.bookings">[number];
function BookingListItem(booking: BookingItem) {
const { t, i18n } = useLocale();
const utils = trpc.useContext();
const mutation = useMutation(
async (confirm: boolean) => {
const res = await fetch("/api/book/confirm", {
method: "PATCH",
body: JSON.stringify({ id: booking.id, confirmed: confirm, language: i18n.language }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new HttpError({ statusCode: res.status });
}
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.bookings"]);
},
}
);
const isUpcoming = new Date(booking.endTime) >= new Date();
const isCancelled = booking.status === BookingStatus.CANCELLED;
const pendingActions: ActionType[] = [
{
id: "reject",
label: t("reject"),
onClick: () => mutation.mutate(false),
icon: BanIcon,
disabled: mutation.isLoading,
},
{
id: "confirm",
label: t("confirm"),
onClick: () => mutation.mutate(true),
icon: CheckIcon,
disabled: mutation.isLoading,
color: "primary",
},
];
const bookedActions: ActionType[] = [
{
id: "cancel",
label: t("cancel"),
href: `/cancel/${booking.uid}`,
icon: XIcon,
},
{
id: "reschedule",
label: t("reschedule"),
href: `/reschedule/${booking.uid}`,
icon: ClockIcon,
},
];
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
return (
<tr>
<td className="hidden px-6 py-4 align-top sm:table-cell whitespace-nowrap">
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
<div className="text-sm text-gray-500">
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
</div>
</td>
<td className={"px-6 py-4" + (booking.rejected ? " line-through" : "")}>
<div className="sm:hidden">
{!booking.confirmed && !booking.rejected && (
<span className="mb-2 inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-800">
{t("unconfirmed")}
</span>
)}
<div className="text-sm font-medium text-gray-900">
{startTime}:{" "}
<small className="text-sm text-gray-500">
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
</small>
</div>
</div>
<div className="text-sm font-medium leading-6 truncate text-neutral-900 max-w-52 md:max-w-96">
{booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>}
{booking.title}
{!booking.confirmed && !booking.rejected && (
<span className="ml-2 hidden sm:inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-800">
{t("unconfirmed")}
</span>
)}
</div>
{booking.description && (
<div className="text-sm text-gray-500 truncate max-w-52 md:max-w-96" title={booking.description}>
&quot;{booking.description}&quot;
</div>
)}
{booking.attendees.length !== 0 && (
<div className="text-sm text-gray-900 hover:text-blue-500">
<a href={"mailto:" + booking.attendees[0].email}>{booking.attendees[0].email}</a>
</div>
)}
</td>
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
{isUpcoming && !isCancelled ? (
<>
{!booking.confirmed && !booking.rejected && <TableActions actions={pendingActions} />}
{booking.confirmed && !booking.rejected && <TableActions actions={bookedActions} />}
{!booking.confirmed && booking.rejected && (
<div className="text-sm text-gray-500">{t("rejected")}</div>
)}
</>
) : null}
</td>
</tr>
);
}
export default BookingListItem;

View File

@@ -1,17 +1,21 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
import { useEffect, useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import utc from "dayjs/plugin/utc";
// Then, include dayjs-business-time
import dayjsBusinessTime from "dayjs-business-time";
import timezone from "dayjs/plugin/timezone";
import getSlots from "@lib/slots";
import dayjsBusinessDays from "dayjs-business-days";
import classNames from "@lib/classNames";
import utc from "dayjs/plugin/utc";
import { useEffect, useState } from "react";
dayjs.extend(dayjsBusinessDays);
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import getSlots from "@lib/slots";
dayjs.extend(dayjsBusinessTime);
dayjs.extend(utc);
dayjs.extend(timezone);
const DatePicker = ({
// FIXME prop types
function DatePicker({
weekStart,
onDatePicked,
workingHours,
@@ -24,7 +28,8 @@ const DatePicker = ({
periodDays,
periodCountCalendarDays,
minimumBookingNotice,
}) => {
}: any): JSX.Element {
const { t } = useLocale();
const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]);
const [selectedMonth, setSelectedMonth] = useState<number | null>(
@@ -44,11 +49,11 @@ const DatePicker = ({
// Handle month changes
const incrementMonth = () => {
setSelectedMonth(selectedMonth + 1);
setSelectedMonth((selectedMonth ?? 0) + 1);
};
const decrementMonth = () => {
setSelectedMonth(selectedMonth - 1);
setSelectedMonth((selectedMonth ?? 0) - 1);
};
const inviteeDate = (): Dayjs => (date || dayjs()).month(selectedMonth);
@@ -69,9 +74,9 @@ const DatePicker = ({
case "rolling": {
const periodRollingEndDay = periodCountCalendarDays
? dayjs().tz(organizerTimeZone).add(periodDays, "days").endOf("day")
: dayjs().tz(organizerTimeZone).businessDaysAdd(periodDays, "days").endOf("day");
: dayjs().tz(organizerTimeZone).addBusinessTime(periodDays, "days").endOf("day");
return (
date.endOf("day").isBefore(dayjs().utcOffsett(date.utcOffset())) ||
date.endOf("day").isBefore(dayjs().utcOffset(date.utcOffset())) ||
date.endOf("day").isAfter(periodRollingEndDay) ||
!getSlots({
inviteeDate: date,
@@ -134,29 +139,35 @@ const DatePicker = ({
}>
<div className="flex text-gray-600 font-light text-xl mb-4">
<span className="w-1/2 text-gray-600 dark:text-white">
<strong className="text-gray-900 dark:text-white">{inviteeDate().format("MMMM")}</strong>
<span className="text-gray-500"> {inviteeDate().format("YYYY")}</span>
<strong className="text-gray-900 dark:text-white">
{t(inviteeDate().format("MMMM").toLowerCase())}
</strong>{" "}
<span className="text-gray-500">{inviteeDate().format("YYYY")}</span>
</span>
<div className="w-1/2 text-right text-gray-600 dark:text-gray-400">
<button
onClick={decrementMonth}
className={
"group mr-2 p-1" + (selectedMonth <= dayjs().month() && "text-gray-400 dark:text-gray-600")
}
disabled={selectedMonth <= dayjs().month()}>
className={classNames(
"group mr-2 p-1",
typeof selectedMonth === "number" &&
selectedMonth <= dayjs().month() &&
"text-gray-400 dark:text-gray-600"
)}
disabled={typeof selectedMonth === "number" && selectedMonth <= dayjs().month()}
data-testid="decrementMonth">
<ChevronLeftIcon className="group-hover:text-black dark:group-hover:text-white w-5 h-5" />
</button>
<button className="group p-1" onClick={incrementMonth}>
<button className="group p-1" onClick={incrementMonth} data-testid="incrementMonth">
<ChevronRightIcon className="group-hover:text-black dark:group-hover:text-white w-5 h-5" />
</button>
</div>
</div>
<div className="grid grid-cols-7 gap-4 text-center border-b border-t dark:border-gray-800 sm:border-0">
{["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
{["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
.sort((a, b) => (weekStart.startsWith(a) ? -1 : weekStart.startsWith(b) ? 1 : 0))
.map((weekDay) => (
<div key={weekDay} className="uppercase text-gray-500 text-xs tracking-widest my-4">
{weekDay}
{t(weekDay.toLowerCase()).substring(0, 3)}
</div>
))}
</div>
@@ -176,16 +187,18 @@ const DatePicker = ({
disabled={day.disabled}
className={classNames(
"absolute w-full top-0 left-0 right-0 bottom-0 rounded-sm text-center mx-auto",
"hover:border hover:border-black dark:hover:border-white",
"hover:border hover:border-brand dark:hover:border-white",
day.disabled
? "text-gray-400 font-light hover:border-0 cursor-default"
: "dark:text-white text-primary-500 font-medium",
date && date.isSame(inviteeDate().date(day.date), "day")
? "bg-black text-white-important"
? "bg-brand text-white-important"
: !day.disabled
? " bg-gray-100 dark:bg-gray-600"
: ""
)}>
)}
data-testid="day"
data-disabled={day.disabled}>
{day.date}
</button>
)}
@@ -194,6 +207,6 @@ const DatePicker = ({
</div>
</div>
);
};
}
export default DatePicker;

View File

@@ -1,96 +0,0 @@
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import getSlots from "../../lib/slots";
import dayjs, { Dayjs } from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import utc from "dayjs/plugin/utc";
dayjs.extend(isBetween);
dayjs.extend(utc);
type Props = {
eventLength: number;
minimumBookingNotice?: number;
date: Dayjs;
workingHours: [];
organizerTimeZone: string;
};
const Slots = ({ eventLength, minimumBookingNotice, date, workingHours, organizerTimeZone }: Props) => {
minimumBookingNotice = minimumBookingNotice || 0;
const router = useRouter();
const { user } = router.query;
const [slots, setSlots] = useState([]);
const [isFullyBooked, setIsFullyBooked] = useState(false);
const [hasErrors, setHasErrors] = useState(false);
useEffect(() => {
setSlots([]);
setIsFullyBooked(false);
setHasErrors(false);
fetch(
`/api/availability/${user}?dateFrom=${date.startOf("day").format()}&dateTo=${date
.endOf("day")
.format()}`
)
.then((res) => res.json())
.then(handleAvailableSlots)
.catch((e) => {
console.error(e);
setHasErrors(true);
});
}, [date]);
const handleAvailableSlots = (busyTimes: []) => {
const times = getSlots({
frequency: eventLength,
inviteeDate: date,
workingHours,
minimumBookingNotice,
organizerTimeZone,
});
const timesLengthBeforeConflicts: number = times.length;
// Check for conflicts
for (let i = times.length - 1; i >= 0; i -= 1) {
busyTimes.every((busyTime): boolean => {
const startTime = dayjs(busyTime.start).utc();
const endTime = dayjs(busyTime.end).utc();
// Check if start times are the same
if (times[i].utc().isSame(startTime)) {
times.splice(i, 1);
}
// Check if time is between start and end times
else if (times[i].utc().isBetween(startTime, endTime)) {
times.splice(i, 1);
}
// Check if slot end time is between start and end time
else if (times[i].utc().add(eventLength, "minutes").isBetween(startTime, endTime)) {
times.splice(i, 1);
}
// Check if startTime is between slot
else if (startTime.isBetween(times[i].utc(), times[i].utc().add(eventLength, "minutes"))) {
times.splice(i, 1);
} else {
return true;
}
return false;
});
}
if (times.length === 0 && timesLengthBeforeConflicts !== 0) {
setIsFullyBooked(true);
}
// Display available times
setSlots(times);
};
return {
slots,
isFullyBooked,
hasErrors,
};
};
export default Slots;

View File

@@ -1,13 +1,22 @@
// TODO: replace headlessui with radix-ui
import { Switch } from "@headlessui/react";
import TimezoneSelect from "react-timezone-select";
import { useEffect, useState } from "react";
import { is24h, timeZone } from "../../lib/clock";
import classNames from "@lib/classNames";
import { FC, useEffect, useState } from "react";
import TimezoneSelect, { ITimezoneOption } from "react-timezone-select";
const TimeOptions = (props) => {
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import { is24h, timeZone } from "../../lib/clock";
type Props = {
onSelectTimeZone: (selectedTimeZone: string) => void;
onToggle24hClock: (is24hClock: boolean) => void;
};
const TimeOptions: FC<Props> = (props) => {
const [selectedTimeZone, setSelectedTimeZone] = useState("");
const [is24hClock, setIs24hClock] = useState(false);
const { t } = useLocale();
useEffect(() => {
setIs24hClock(is24h());
@@ -25,47 +34,45 @@ const TimeOptions = (props) => {
props.onToggle24hClock(is24h(is24hClock));
};
return (
selectedTimeZone !== "" && (
<div className="absolute z-10 w-full max-w-80 rounded-sm border border-gray-200 dark:bg-gray-700 dark:border-0 bg-white px-4 py-2">
<div className="flex mb-4">
<div className="w-1/2 dark:text-white text-gray-600 font-medium">Time Options</div>
<div className="w-1/2">
<Switch.Group as="div" className="flex items-center justify-end">
<Switch.Label as="span" className="mr-3">
<span className="text-sm dark:text-white text-gray-500">am/pm</span>
</Switch.Label>
<Switch
checked={is24hClock}
onChange={handle24hClockToggle}
return selectedTimeZone !== "" ? (
<div className="absolute z-10 w-full max-w-80 rounded-sm border border-gray-200 dark:bg-gray-700 dark:border-0 bg-white px-4 py-2">
<div className="flex mb-4">
<div className="w-1/2 dark:text-white text-gray-600 font-medium">{t("time_options")}</div>
<div className="w-1/2">
<Switch.Group as="div" className="flex items-center justify-end">
<Switch.Label as="span" className="mr-3">
<span className="text-sm dark:text-white text-gray-500">{t("am_pm")}</span>
</Switch.Label>
<Switch
checked={is24hClock}
onChange={handle24hClockToggle}
className={classNames(
is24hClock ? "bg-brand" : "dark:bg-gray-600 bg-gray-200",
"relative inline-flex flex-shrink-0 h-5 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
)}>
<span className="sr-only">{t("use_setting")}</span>
<span
aria-hidden="true"
className={classNames(
is24hClock ? "bg-black" : "dark:bg-gray-600 bg-gray-200",
"relative inline-flex flex-shrink-0 h-5 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
)}>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={classNames(
is24hClock ? "translate-x-3" : "translate-x-0",
"pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)}
/>
</Switch>
<Switch.Label as="span" className="ml-3">
<span className="text-sm dark:text-white text-gray-500">24h</span>
</Switch.Label>
</Switch.Group>
</div>
is24hClock ? "translate-x-3" : "translate-x-0",
"pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)}
/>
</Switch>
<Switch.Label as="span" className="ml-3">
<span className="text-sm dark:text-white text-gray-500">{t("24_h")}</span>
</Switch.Label>
</Switch.Group>
</div>
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={(tz) => setSelectedTimeZone(tz.value)}
className="mb-2 shadow-sm focus:ring-black focus:border-black mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
/>
</div>
)
);
<TimezoneSelect
id="timeZone"
value={selectedTimeZone}
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
className="mb-2 shadow-sm focus:ring-black focus:border-brand mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
/>
</div>
) : null;
};
export default TimeOptions;

View File

@@ -1,41 +1,41 @@
// Get router variables
import { useRouter } from "next/router";
import { useEffect, useState, useMemo } from "react";
import { EventType } from "@prisma/client";
import { ChevronDownIcon, ChevronUpIcon, ClockIcon, CreditCardIcon, GlobeIcon } from "@heroicons/react/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import dayjs, { Dayjs } from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import utc from "dayjs/plugin/utc";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { ChevronDownIcon, ChevronUpIcon, ClockIcon, GlobeIcon } from "@heroicons/react/solid";
import DatePicker from "@components/booking/DatePicker";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import PoweredByCalendso from "@components/ui/PoweredByCalendso";
import { timeZone } from "@lib/clock";
import AvailableTimes from "@components/booking/AvailableTimes";
import TimeOptions from "@components/booking/TimeOptions";
import * as Collapsible from "@radix-ui/react-collapsible";
import { HeadSeo } from "@components/seo/head-seo";
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { useLocale } from "@lib/hooks/useLocale";
import useTheme from "@lib/hooks/useTheme";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import CustomBranding from "@components/CustomBranding";
import AvailableTimes from "@components/booking/AvailableTimes";
import DatePicker from "@components/booking/DatePicker";
import TimeOptions from "@components/booking/TimeOptions";
import { HeadSeo } from "@components/seo/head-seo";
import AvatarGroup from "@components/ui/AvatarGroup";
import PoweredByCal from "@components/ui/PoweredByCal";
import { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
dayjs.extend(utc);
dayjs.extend(customParseFormat);
type AvailabilityPageProps = {
eventType: EventType;
profile: {
name: string;
image: string;
theme?: string;
};
workingHours: [];
};
type Props = AvailabilityTeamPageProps | AvailabilityPageProps;
const AvailabilityPage = ({ profile, eventType, workingHours }: AvailabilityPageProps) => {
const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
const router = useRouter();
const { rescheduleUid } = router.query;
const themeLoaded = useTheme(profile.theme);
const { isReady } = useTheme(profile.theme);
const { t } = useLocale();
const selectedDate = useMemo(() => {
const dateString = asStringOrNull(router.query.date);
@@ -89,20 +89,21 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: AvailabilityPage
};
return (
themeLoaded && (
<>
<HeadSeo
title={`${rescheduleUid ? "Reschedule" : ""} ${eventType.title} | ${profile.name}`}
description={`${rescheduleUid ? "Reschedule" : ""} ${eventType.title}`}
name={profile.name}
avatar={profile.image}
/>
<div>
<main
className={
"mx-auto my-0 md:my-24 transition-max-width ease-in-out duration-500 " +
(selectedDate ? "max-w-5xl" : "max-w-3xl")
}>
<>
<HeadSeo
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
name={profile.name}
avatar={profile.image}
/>
<CustomBranding val={profile.brandColor} />
<div>
<main
className={
"mx-auto my-0 md:my-24 transition-max-width ease-in-out duration-500 " +
(selectedDate ? "max-w-5xl" : "max-w-3xl")
}>
{isReady && (
<div className="bg-white border-gray-200 rounded-sm sm:dark:border-gray-600 dark:bg-gray-900 md:border">
{/* mobile: details */}
<div className="block p-4 sm:p-8 md:hidden">
@@ -125,8 +126,20 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: AvailabilityPage
{eventType.title}
<div>
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
{eventType.length} minutes
{eventType.length} {t("minutes")}
</div>
{eventType.price > 0 && (
<div>
<CreditCardIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</IntlProvider>
</div>
)}
</div>
</div>
</div>
@@ -151,14 +164,26 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: AvailabilityPage
size={10}
truncateAfter={3}
/>
<h2 className="font-medium text-gray-500 dark:text-gray-300 mt-3">{profile.name}</h2>
<h1 className="mb-4 text-3xl font-semibold text-gray-800 dark:text-white">
<h2 className="mt-3 font-medium text-gray-500 dark:text-gray-300">{profile.name}</h2>
<h1 className="mb-4 text-3xl font-semibold text-gray-800 font-cal dark:text-white">
{eventType.title}
</h1>
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
{eventType.length} minutes
{eventType.length} {t("minutes")}
</p>
{eventType.price > 0 && (
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
<CreditCardIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</IntlProvider>
</p>
)}
<TimezoneDropdown />
@@ -172,14 +197,8 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: AvailabilityPage
periodDays={eventType?.periodDays}
periodCountCalendarDays={eventType?.periodCountCalendarDays}
onDatePicked={changeDate}
workingHours={[
{
days: [0, 1, 2, 3, 4, 5, 6],
endTime: 1440,
startTime: 0,
},
]}
weekStart="Sunday"
workingHours={workingHours}
weekStart={profile.weekStart || "Sunday"}
eventLength={eventType.length}
minimumBookingNotice={eventType.minimumBookingNotice}
/>
@@ -202,11 +221,11 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: AvailabilityPage
)}
</div>
</div>
{eventType.users.length && isBrandingHidden(eventType.users[0]) && <PoweredByCalendso />}
</main>
</div>
</>
)
)}
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && <PoweredByCal />}
</main>
</div>
</>
);
function TimezoneDropdown() {

View File

@@ -1,25 +1,46 @@
import {
CalendarIcon,
ClockIcon,
CreditCardIcon,
ExclamationIcon,
LocationMarkerIcon,
} from "@heroicons/react/solid";
import { EventTypeCustomInputType } from "@prisma/client";
import dayjs from "dayjs";
import Head from "next/head";
import { useRouter } from "next/router";
import { CalendarIcon, ClockIcon, ExclamationIcon, LocationMarkerIcon } from "@heroicons/react/solid";
import { EventTypeCustomInputType } from "@prisma/client";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { useEffect, useState } from "react";
import dayjs from "dayjs";
import "react-phone-number-input/style.css";
import PhoneInput from "react-phone-number-input";
import { LocationType } from "@lib/location";
import { Button } from "@components/ui/Button";
import { stringify } from "querystring";
import { useCallback, useEffect, useState } from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import { ReactMultiEmail } from "react-multi-email";
import { createPaymentLink } from "@ee/lib/stripe/client";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
import { useLocale } from "@lib/hooks/useLocale";
import useTheme from "@lib/hooks/useTheme";
import AvatarGroup from "@components/ui/AvatarGroup";
import { LocationType } from "@lib/location";
import createBooking from "@lib/mutations/bookings/create-booking";
import { parseZone } from "@lib/parseZone";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { BookingCreateBody } from "@lib/types/booking";
const BookingPage = (props: any): JSX.Element => {
import CustomBranding from "@components/CustomBranding";
import AvatarGroup from "@components/ui/AvatarGroup";
import { Button } from "@components/ui/Button";
import PhoneInput from "@components/ui/form/PhoneInput";
import { BookPageProps } from "../../../pages/[user]/book";
import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
type BookingPageProps = BookPageProps | TeamBookingPageProps;
const BookingPage = (props: BookingPageProps) => {
const { t, i18n } = useLocale();
const router = useRouter();
const { rescheduleUid } = router.query;
const themeLoaded = useTheme(props.profile.theme);
const { isReady } = useTheme(props.profile.theme);
const date = asStringOrNull(router.query.date);
const timeFormat = asStringOrNull(router.query.clock) === "24h" ? "H:mm" : "h:mma";
@@ -48,13 +69,14 @@ const BookingPage = (props: any): JSX.Element => {
// TODO: Move to translations
const locationLabels = {
[LocationType.InPerson]: "Link or In-person meeting",
[LocationType.Phone]: "Phone call",
[LocationType.InPerson]: t("in_person_meeting"),
[LocationType.Phone]: t("phone_call"),
[LocationType.GoogleMeet]: "Google Meet",
[LocationType.Zoom]: "Zoom Video",
[LocationType.Daily]: "Daily.co Video",
};
const bookingHandler = (event) => {
const _bookingHandler = (event) => {
const book = async () => {
setLoading(true);
setError(false);
@@ -65,7 +87,7 @@ const BookingPage = (props: any): JSX.Element => {
const data = event.target["custom_" + input.id];
if (data) {
if (input.type === EventTypeCustomInputType.BOOL) {
return input.label + "\n" + (data.checked ? "Yes" : "No");
return input.label + "\n" + (data.checked ? t("yes") : t("no"));
} else {
return input.label + "\n" + data.value;
}
@@ -74,12 +96,12 @@ const BookingPage = (props: any): JSX.Element => {
.join("\n\n");
}
if (!!notes && !!event.target.notes.value) {
notes += "\n\nAdditional notes:\n" + event.target.notes.value;
notes += `\n\n${t("additional_notes")}:\n` + event.target.notes.value;
} else {
notes += event.target.notes.value;
}
const payload = {
const payload: BookingCreateBody = {
start: dayjs(date).format(),
end: dayjs(date).add(props.eventType.length, "minute").format(),
name: event.target.name.value,
@@ -87,13 +109,11 @@ const BookingPage = (props: any): JSX.Element => {
notes: notes,
guests: guestEmails,
eventTypeId: props.eventType.id,
rescheduleUid: rescheduleUid,
timeZone: timeZone(),
language: i18n.language,
};
if (router.query.user) {
payload.user = router.query.user;
}
if (typeof rescheduleUid === "string") payload.rescheduleUid = rescheduleUid;
if (typeof router.query.user === "string") payload.user = router.query.user;
if (selectedLocation) {
switch (selectedLocation) {
@@ -115,50 +135,80 @@ const BookingPage = (props: any): JSX.Element => {
jitsu.track(telemetryEventTypes.bookingConfirmed, collectPageParameters())
);
/*const res = await */ fetch("/api/book/event", {
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
},
method: "POST",
const content = await createBooking(payload).catch((e) => {
console.error(e.message);
setLoading(false);
setError(true);
});
// TODO When the endpoint is fixed, change this to await the result again
//if (res.ok) {
let successUrl = `/success?date=${encodeURIComponent(date)}&type=${props.eventType.id}&user=${
props.profile.slug
}&reschedule=${!!rescheduleUid}&name=${payload.name}`;
if (payload["location"]) {
if (payload["location"].includes("integration")) {
successUrl += "&location=" + encodeURIComponent("Web conferencing details to follow.");
} else {
successUrl += "&location=" + encodeURIComponent(payload["location"]);
}
}
await router.push(successUrl);
if (content?.id) {
const params: { [k: string]: any } = {
date,
type: props.eventType.id,
user: props.profile.slug,
reschedule: !!rescheduleUid,
name: payload.name,
email: payload.email,
};
if (payload["location"]) {
if (payload["location"].includes("integration")) {
params.location = t("web_conferencing_details_to_follow");
} else {
params.location = payload["location"];
}
}
const query = stringify(params);
let successUrl = `/success?${query}`;
if (content?.paymentUid) {
successUrl = createPaymentLink({
paymentUid: content?.paymentUid,
name: payload.name,
date,
absolute: false,
});
}
await router.push(successUrl);
} else {
setLoading(false);
setError(true);
}
};
event.preventDefault();
book();
};
return (
themeLoaded && (
<div>
<Head>
<title>
{rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with {props.profile.name}{" "}
| Cal.com
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
const bookingHandler = useCallback(_bookingHandler, [guestEmails]);
<main className="max-w-3xl mx-auto my-0 sm:my-24">
<div className="dark:bg-neutral-900 bg-white overflow-hidden border border-gray-200 dark:border-0 sm:rounded-sm">
<div className="sm:flex px-4 py-5 sm:p-4">
<div className="sm:w-1/2 sm:border-r sm:dark:border-black">
return (
<div>
<Head>
<title>
{rescheduleUid
? t("booking_reschedule_confirmation", {
eventTypeTitle: props.eventType.title,
profileName: props.profile.name,
})
: t("booking_confirmation", {
eventTypeTitle: props.eventType.title,
profileName: props.profile.name,
})}{" "}
| Cal.com
</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<CustomBranding val={props.profile.brandColor} />
<main className="max-w-3xl mx-auto my-0 rounded-sm sm:my-24 sm:border sm:dark:border-gray-600">
{isReady && (
<div className="overflow-hidden bg-white border border-gray-200 dark:bg-neutral-900 dark:border-0 sm:rounded-sm">
<div className="px-4 py-5 sm:flex sm:p-4">
<div className="sm:w-1/2 sm:border-r sm:dark:border-gray-800">
<AvatarGroup
size={16}
size={14}
items={[{ image: props.profile.image, alt: props.profile.name }].concat(
props.eventType.users
.filter((user) => user.name !== props.profile.name)
@@ -168,31 +218,45 @@ const BookingPage = (props: any): JSX.Element => {
}))
)}
/>
<h2 className="font-medium dark:text-gray-300 text-gray-500">{props.profile.name}</h2>
<h1 className="text-3xl font-semibold dark:text-white text-gray-800 mb-4">
<h2 className="mt-2 font-medium text-gray-500 font-cal dark:text-gray-300">
{props.profile.name}
</h2>
<h1 className="mb-4 text-3xl font-semibold text-gray-800 dark:text-white">
{props.eventType.title}
</h1>
<p className="text-gray-500 mb-2">
<p className="mb-2 text-gray-500">
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
{props.eventType.length} minutes
{props.eventType.length} {t("minutes")}
</p>
{props.eventType.price > 0 && (
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
<CreditCardIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
<IntlProvider locale="en">
<FormattedNumber
value={props.eventType.price / 100.0}
style="currency"
currency={props.eventType.currency.toUpperCase()}
/>
</IntlProvider>
</p>
)}
{selectedLocation === LocationType.InPerson && (
<p className="text-gray-500 mb-2">
<p className="mb-2 text-gray-500">
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
{locationInfo(selectedLocation).address}
</p>
)}
<p className="text-green-500 mb-4">
<p className="mb-4 text-green-500">
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
{parseZone(date).format(timeFormat + ", dddd DD MMMM YYYY")}
</p>
<p className="dark:text-white text-gray-600 mb-8">{props.eventType.description}</p>
<p className="mb-8 text-gray-600 dark:text-white">{props.eventType.description}</p>
</div>
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
<form onSubmit={bookingHandler}>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium dark:text-white text-gray-700">
Your name
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
{t("your_name")}
</label>
<div className="mt-1">
<input
@@ -200,7 +264,7 @@ const BookingPage = (props: any): JSX.Element => {
name="name"
id="name"
required
className="shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-md"
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
placeholder="John Doe"
defaultValue={props.booking ? props.booking.attendees[0].name : ""}
/>
@@ -209,16 +273,17 @@ const BookingPage = (props: any): JSX.Element => {
<div className="mb-4">
<label
htmlFor="email"
className="block text-sm font-medium dark:text-white text-gray-700">
Email address
className="block text-sm font-medium text-gray-700 dark:text-white">
{t("email_address")}
</label>
<div className="mt-1">
<input
type="email"
name="email"
id="email"
inputMode="email"
required
className="shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-md"
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
placeholder="you@example.com"
defaultValue={props.booking ? props.booking.attendees[0].email : ""}
/>
@@ -226,8 +291,8 @@ const BookingPage = (props: any): JSX.Element => {
</div>
{locations.length > 1 && (
<div className="mb-4">
<span className="block text-sm font-medium dark:text-white text-gray-700">
Location
<span className="block text-sm font-medium text-gray-700 dark:text-white">
{t("location")}
</span>
{locations.map((location) => (
<label key={location.type} className="block">
@@ -235,12 +300,12 @@ const BookingPage = (props: any): JSX.Element => {
type="radio"
required
onChange={(e) => setSelectedLocation(e.target.value)}
className="location focus:ring-black h-4 w-4 text-black border-gray-300 mr-2"
className="w-4 h-4 mr-2 text-black border-gray-300 location focus:ring-black"
name="location"
value={location.type}
checked={selectedLocation === location.type}
/>
<span className="text-sm ml-2 dark:text-gray-500">
<span className="ml-2 text-sm dark:text-gray-500">
{locationLabels[location.type]}
</span>
</label>
@@ -251,20 +316,11 @@ const BookingPage = (props: any): JSX.Element => {
<div className="mb-4">
<label
htmlFor="phone"
className="block text-sm font-medium dark:text-white text-gray-700">
Phone Number
className="block text-sm font-medium text-gray-700 dark:text-white">
{t("phone_number")}
</label>
<div className="mt-1">
<PhoneInput
name="phone"
placeholder="Enter phone number"
id="phone"
required
className="shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-md"
onChange={() => {
/* DO NOT REMOVE: Callback required by PhoneInput, comment added to satisfy eslint:no-empty-function */
}}
/>
<PhoneInput name="phone" placeholder={t("enter_phone_number")} id="phone" required />
</div>
</div>
)}
@@ -276,7 +332,7 @@ const BookingPage = (props: any): JSX.Element => {
{input.type !== EventTypeCustomInputType.BOOL && (
<label
htmlFor={"custom_" + input.id}
className="block text-sm font-medium text-gray-700 dark:text-white mb-1">
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
{input.label}
</label>
)}
@@ -286,7 +342,7 @@ const BookingPage = (props: any): JSX.Element => {
id={"custom_" + input.id}
required={input.required}
rows={3}
className="shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-md"
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
placeholder={input.placeholder}
/>
)}
@@ -296,7 +352,7 @@ const BookingPage = (props: any): JSX.Element => {
name={"custom_" + input.id}
id={"custom_" + input.id}
required={input.required}
className="shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-md"
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
placeholder={input.placeholder}
/>
)}
@@ -306,7 +362,7 @@ const BookingPage = (props: any): JSX.Element => {
name={"custom_" + input.id}
id={"custom_" + input.id}
required={input.required}
className="shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-md"
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
placeholder=""
/>
)}
@@ -316,88 +372,96 @@ const BookingPage = (props: any): JSX.Element => {
type="checkbox"
name={"custom_" + input.id}
id={"custom_" + input.id}
className="focus:ring-black h-4 w-4 text-black border-gray-300 rounded mr-2"
className="w-4 h-4 mr-2 text-black border-gray-300 rounded focus:ring-black"
placeholder=""
required={input.required}
/>
<label
htmlFor={"custom_" + input.id}
className="block text-sm font-medium text-gray-700 dark:text-white mb-1">
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
{input.label}
</label>
</div>
)}
</div>
))}
<div className="mb-4">
{!guestToggle && (
<label
onClick={toggleGuestEmailInput}
htmlFor="guests"
className="block text-sm font-medium dark:text-white text-blue-500 mb-1 hover:cursor-pointer">
+ Additional Guests
</label>
)}
{guestToggle && (
<div>
{!props.eventType.disableGuests && (
<div className="mb-4">
{!guestToggle && (
<label
onClick={toggleGuestEmailInput}
htmlFor="guests"
className="block text-sm font-medium dark:text-white text-gray-700 mb-1">
Guests
className="block mb-1 text-sm font-medium text-blue-500 dark:text-white hover:cursor-pointer">
{t("additional_guests")}
</label>
<ReactMultiEmail
placeholder="guest@example.com"
emails={guestEmails}
onChange={(_emails: string[]) => {
setGuestEmails(_emails);
}}
getLabel={(email: string, index: number, removeEmail: (index: number) => void) => {
return (
<div data-tag key={index}>
{email}
<span data-tag-handle onClick={() => removeEmail(index)}>
×
</span>
</div>
);
}}
/>
</div>
)}
</div>
)}
{guestToggle && (
<div>
<label
htmlFor="guests"
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
{t("guests")}
</label>
<ReactMultiEmail
className="relative"
placeholder="guest@example.com"
emails={guestEmails}
onChange={(_emails: string[]) => {
setGuestEmails(_emails);
}}
getLabel={(
email: string,
index: number,
removeEmail: (index: number) => void
) => {
return (
<div data-tag key={index}>
{email}
<span data-tag-handle onClick={() => removeEmail(index)}>
×
</span>
</div>
);
}}
/>
</div>
)}
</div>
)}
<div className="mb-4">
<label
htmlFor="notes"
className="block text-sm font-medium dark:text-white text-gray-700 mb-1">
Additional notes
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
{t("additional_notes")}
</label>
<textarea
name="notes"
id="notes"
rows={3}
className="shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-md"
placeholder="Please share anything that will help prepare for our meeting."
className="block w-full border-gray-300 rounded-sm shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-brand sm:text-sm"
placeholder={t("share_additional_notes")}
defaultValue={props.booking ? props.booking.description : ""}
/>
</div>
<div className="flex items-start space-x-2">
{/* TODO: add styling props to <Button variant="" color="" /> and get rid of btn-primary */}
<Button type="submit" loading={loading}>
{rescheduleUid ? "Reschedule" : "Confirm"}
{rescheduleUid ? t("reschedule") : t("confirm")}
</Button>
<Button color="secondary" type="button" onClick={() => router.back()}>
Cancel
{t("cancel")}
</Button>
</div>
</form>
{error && (
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mt-2">
<div className="p-4 mt-2 border-l-4 border-yellow-400 bg-yellow-50">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
<ExclamationIcon className="w-5 h-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">
Could not {rescheduleUid ? "reschedule" : "book"} the meeting.
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}
</p>
</div>
</div>
@@ -406,9 +470,9 @@ const BookingPage = (props: any): JSX.Element => {
</div>
</div>
</div>
</main>
</div>
)
)}
</main>
</div>
);
};

View File

@@ -1,19 +1,31 @@
import { DialogClose, DialogContent } from "@components/Dialog";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { ExclamationIcon } from "@heroicons/react/outline";
import { CheckIcon } from "@heroicons/react/solid";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import React, { PropsWithChildren } from "react";
import { useLocale } from "@lib/hooks/useLocale";
import { DialogClose, DialogContent } from "@components/Dialog";
import { Button } from "@components/ui/Button";
export type ConfirmationDialogContentProps = {
confirmBtnText?: string;
cancelBtnText?: string;
onConfirm: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
onConfirm?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
title: string;
variety?: "danger" /* no others yet */;
variety?: "danger" | "warning" | "success";
};
export default function ConfirmationDialogContent(props: PropsWithChildren<ConfirmationDialogContentProps>) {
const { title, variety, confirmBtnText = "Confirm", cancelBtnText = "Cancel", onConfirm, children } = props;
const { t } = useLocale();
const {
title,
variety,
confirmBtnText = t("confirm"),
cancelBtnText = t("cancel"),
onConfirm,
children,
} = props;
return (
<DialogContent>
@@ -25,14 +37,28 @@ export default function ConfirmationDialogContent(props: PropsWithChildren<Confi
<ExclamationIcon className="w-5 h-5 text-red-600" />
</div>
)}
{variety === "warning" && (
<div className="text-center p-2 rounded-full mx-auto bg-orange-100">
<ExclamationIcon className="w-5 h-5 text-orange-600" />
</div>
)}
{variety === "success" && (
<div className="text-center p-2 rounded-full mx-auto bg-green-100">
<CheckIcon className="w-5 h-5 text-green-600" />
</div>
)}
</div>
)}
<div>
<DialogPrimitive.Title className="text-xl font-bold text-gray-900">{title}</DialogPrimitive.Title>
<DialogPrimitive.Description className="text-neutral-500">{children}</DialogPrimitive.Description>
<DialogPrimitive.Title className="font-cal text-xl font-bold text-gray-900">
{title}
</DialogPrimitive.Title>
<DialogPrimitive.Description className="text-neutral-500 text-sm">
{children}
</DialogPrimitive.Description>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-x-2">
<div className="mt-5 sm:mt-8 sm:flex sm:flex-row-reverse gap-x-2">
<DialogClose onClick={onConfirm} asChild>
<Button color="primary">{confirmBtnText}</Button>
</DialogClose>

View File

@@ -1,4 +1,5 @@
import React from "react";
import { HttpError } from "@lib/core/http/error";
type Props = {

View File

@@ -1,7 +1,24 @@
import { EventType, SchedulingType } from "@prisma/client";
import { ClockIcon, InformationCircleIcon, UserIcon, UsersIcon } from "@heroicons/react/solid";
import { ClockIcon, CreditCardIcon, UserIcon, UsersIcon } from "@heroicons/react/solid";
import { SchedulingType } from "@prisma/client";
import { Prisma } from "@prisma/client";
import React from "react";
import { FormattedNumber, IntlProvider } from "react-intl";
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
select: {
id: true,
length: true,
price: true,
currency: true,
schedulingType: true,
description: true,
},
});
type EventType = Prisma.EventTypeGetPayload<typeof eventTypeData>;
export type EventTypeDescriptionProps = {
eventType: EventType;
@@ -9,34 +26,48 @@ export type EventTypeDescriptionProps = {
};
export const EventTypeDescription = ({ eventType, className }: EventTypeDescriptionProps) => {
const { t } = useLocale();
return (
<ul className={classNames("mt-2 space-x-4 text-neutral-500 dark:text-white flex", className)}>
<li className="flex whitespace-nowrap">
<ClockIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
{eventType.length}m
</li>
{eventType.schedulingType ? (
<li className="flex whitespace-nowrap">
<UsersIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && "Round Robin"}
{eventType.schedulingType === SchedulingType.COLLECTIVE && "Collective"}
</li>
) : (
<li className="flex whitespace-nowrap">
<UserIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
1-on-1
</li>
)}
{eventType.description && (
<li className="flex">
<InformationCircleIcon
className="flex-none inline mr-1.5 mt-0.5 h-4 w-4 text-neutral-400"
aria-hidden="true"
/>
<span>{eventType.description.substring(0, 100)}</span>
</li>
)}
</ul>
<>
<div className={classNames("text-neutral-500 dark:text-white", className)}>
{eventType.description && (
<h2 className="opacity-60 truncate max-w-[280px] sm:max-w-[500px]">
{eventType.description.substring(0, 100)}
</h2>
)}
<ul className="flex mt-2 space-x-4 ">
<li className="flex whitespace-nowrap">
<ClockIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
{eventType.length}m
</li>
{eventType.schedulingType ? (
<li className="flex whitespace-nowrap">
<UsersIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
</li>
) : (
<li className="flex whitespace-nowrap">
<UserIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
{t("1_on_1")}
</li>
)}
{eventType.price > 0 && (
<li className="flex whitespace-nowrap">
<CreditCardIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
<IntlProvider locale="en">
<FormattedNumber
value={eventType.price / 100.0}
style="currency"
currency={eventType.currency.toUpperCase()}
/>
</IntlProvider>
</li>
)}
</ul>
</div>
</>
);
};

142
components/form/fields.tsx Normal file
View File

@@ -0,0 +1,142 @@
import { useId } from "@radix-ui/react-id";
import { forwardRef, ReactElement, ReactNode, Ref } from "react";
import { FieldValues, FormProvider, SubmitHandler, useFormContext, UseFormReturn } from "react-hook-form";
import classNames from "@lib/classNames";
import { getErrorFromUnknown } from "@lib/errors";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { Alert } from "@components/ui/Alert";
type InputProps = Omit<JSX.IntrinsicElements["input"], "name"> & { name: string };
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return (
<input
{...props}
ref={ref}
className={classNames(
"mt-1 block w-full border border-gray-300 rounded-sm shadow-sm py-2 px-3 focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm",
props.className
)}
/>
);
});
export function Label(props: JSX.IntrinsicElements["label"]) {
return (
<label {...props} className={classNames("block text-sm font-medium text-gray-700", props.className)}>
{props.children}
</label>
);
}
type InputFieldProps = {
label?: ReactNode;
addOnLeading?: ReactNode;
} & React.ComponentProps<typeof Input> & {
labelProps?: React.ComponentProps<typeof Label>;
};
const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(props, ref) {
const id = useId();
const { t } = useLocale();
const methods = useFormContext();
const {
label = t(props.name),
labelProps,
placeholder = t(props.name + "_placeholder") !== props.name + "_placeholder"
? t(props.name + "_placeholder")
: "",
className,
addOnLeading,
...passThroughToInput
} = props;
return (
<div>
<Label htmlFor={id} {...labelProps}>
{label}
</Label>
{addOnLeading ? (
<div className="flex mt-1 rounded-md shadow-sm">
{addOnLeading}
<Input
id={id}
placeholder={placeholder}
className={classNames(className, "mt-0")}
{...passThroughToInput}
ref={ref}
/>
</div>
) : (
<Input id={id} placeholder={placeholder} className={className} {...passThroughToInput} ref={ref} />
)}
{methods?.formState?.errors[props.name] && (
<Alert className="mt-1" severity="error" message={methods.formState.errors[props.name].message} />
)}
</div>
);
});
export const TextField = forwardRef<HTMLInputElement, InputFieldProps>(function TextField(props, ref) {
return <InputField ref={ref} {...props} />;
});
export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(function PasswordField(
props,
ref
) {
return <InputField type="password" placeholder="•••••••••••••" ref={ref} {...props} />;
});
export const EmailField = forwardRef<HTMLInputElement, InputFieldProps>(function EmailField(props, ref) {
return <InputField type="email" inputMode="email" ref={ref} {...props} />;
});
type FormProps<T> = { form: UseFormReturn<T>; handleSubmit: SubmitHandler<T> } & Omit<
JSX.IntrinsicElements["form"],
"onSubmit"
>;
const PlainForm = <T extends FieldValues>(props: FormProps<T>, ref: Ref<HTMLFormElement>) => {
const { form, handleSubmit, ...passThrough } = props;
return (
<FormProvider {...form}>
<form
ref={ref}
onSubmit={(event) => {
form
.handleSubmit(handleSubmit)(event)
.catch((err) => {
showToast(`${getErrorFromUnknown(err).message}`, "error");
});
}}
{...passThrough}>
{props.children}
</form>
</FormProvider>
);
};
export const Form = forwardRef(PlainForm) as <T extends FieldValues>(
p: FormProps<T> & { ref?: Ref<HTMLFormElement> }
) => ReactElement;
export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) {
return (
<legend {...props} className={classNames("text-sm font-medium text-gray-700", props.className)}>
{props.children}
</legend>
);
}
export function InputGroupBox(props: JSX.IntrinsicElements["div"]) {
return (
<div
{...props}
className={classNames("p-2 bg-white border border-gray-300 rounded-sm space-y-2", props.className)}>
{props.children}
</div>
);
}

View File

@@ -0,0 +1,218 @@
import React, { Fragment } from "react";
import { useMutation } from "react-query";
import { QueryCell } from "@lib/QueryCell";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { trpc } from "@lib/trpc";
import { List } from "@components/List";
import { ShellSubHeading } from "@components/Shell";
import { Alert } from "@components/ui/Alert";
import Button from "@components/ui/Button";
import Switch from "@components/ui/Switch";
import ConnectIntegration from "./ConnectIntegrations";
import DisconnectIntegration from "./DisconnectIntegration";
import IntegrationListItem from "./IntegrationListItem";
import SubHeadingTitleWithConnections from "./SubHeadingTitleWithConnections";
type Props = {
onChanged: () => unknown | Promise<unknown>;
};
function CalendarSwitch(props: {
type: string;
externalId: string;
title: string;
defaultSelected: boolean;
}) {
const utils = trpc.useContext();
const mutation = useMutation<
unknown,
unknown,
{
isOn: boolean;
}
>(
async ({ isOn }) => {
const body = {
integration: props.type,
externalId: props.externalId,
};
if (isOn) {
const res = await fetch("/api/availability/calendar", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error("Something went wrong");
}
} else {
const res = await fetch("/api/availability/calendar", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error("Something went wrong");
}
}
},
{
async onSettled() {
await utils.invalidateQueries(["viewer.integrations"]);
},
onError() {
showToast(`Something went wrong when toggling "${props.title}""`, "error");
},
}
);
return (
<div className="py-1">
<Switch
key={props.externalId}
name="enabled"
label={props.title}
defaultChecked={props.defaultSelected}
onCheckedChange={(isOn: boolean) => {
mutation.mutate({ isOn });
}}
/>
</div>
);
}
function ConnectedCalendarsList(props: Props) {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.connectedCalendars"], { suspense: true });
return (
<QueryCell
query={query}
empty={() => null}
success={({ data }) => (
<List>
{data.map((item) => (
<Fragment key={item.credentialId}>
{item.calendars ? (
<IntegrationListItem
{...item.integration}
description={item.primary?.externalId || "No external Id"}
actions={
<DisconnectIntegration
id={item.credentialId}
render={(btnProps) => (
<Button {...btnProps} color="warn">
{t("disconnect")}
</Button>
)}
onOpenChange={props.onChanged}
/>
}>
<ul className="p-4 space-y-2">
{item.calendars.map((cal) => (
<CalendarSwitch
key={cal.externalId}
externalId={cal.externalId as string}
title={cal.name as string}
type={item.integration.type}
defaultSelected={cal.isSelected}
/>
))}
</ul>
</IntegrationListItem>
) : (
<Alert
severity="warning"
title="Something went wrong"
message={item.error?.message}
actions={
<DisconnectIntegration
id={item.credentialId}
render={(btnProps) => (
<Button {...btnProps} color="warn">
{t("disconnect")}
</Button>
)}
onOpenChange={() => props.onChanged()}
/>
}
/>
)}
</Fragment>
))}
</List>
)}
/>
);
}
function CalendarList(props: Props) {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations"]);
return (
<QueryCell
query={query}
success={({ data }) => (
<List>
{data.calendar.items.map((item) => (
<IntegrationListItem
key={item.title}
{...item}
actions={
<ConnectIntegration
type={item.type}
render={(btnProps) => (
<Button color="secondary" {...btnProps}>
{t("connect")}
</Button>
)}
onOpenChange={() => props.onChanged()}
/>
}
/>
))}
</List>
)}
/>
);
}
export function CalendarListContainer(props: { heading?: false }) {
const { t } = useLocale();
const { heading = true } = props;
const utils = trpc.useContext();
const onChanged = () =>
Promise.allSettled([
utils.invalidateQueries(["viewer.integrations"]),
utils.invalidateQueries(["viewer.connectedCalendars"]),
]);
const query = trpc.useQuery(["viewer.connectedCalendars"]);
return (
<>
{heading && (
<ShellSubHeading
className="mt-10"
title={<SubHeadingTitleWithConnections title={t("calendar")} numConnections={query.data?.length} />}
subtitle={t("configure_how_your_event_types_interact")}
/>
)}
<ConnectedCalendarsList onChanged={onChanged} />
{!!query.data?.length && (
<ShellSubHeading
className="mt-6"
title={<SubHeadingTitleWithConnections title={t("connect_an_additional_calendar")} />}
/>
)}
<CalendarList onChanged={onChanged} />
</>
);
}

View File

@@ -0,0 +1,63 @@
import type { IntegrationOAuthCallbackState } from "pages/api/integrations/types";
import { useState } from "react";
import { useMutation } from "react-query";
import { AddAppleIntegrationModal } from "@lib/integrations/Apple/components/AddAppleIntegration";
import { AddCalDavIntegrationModal } from "@lib/integrations/CalDav/components/AddCalDavIntegration";
import { ButtonBaseProps } from "@components/ui/Button";
export default function ConnectIntegration(props: {
type: string;
render: (renderProps: ButtonBaseProps) => JSX.Element;
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
}) {
const { type } = props;
const [isLoading, setIsLoading] = useState(false);
const mutation = useMutation(async () => {
const state: IntegrationOAuthCallbackState = {
returnTo: location.pathname + location.search,
};
const stateStr = encodeURIComponent(JSON.stringify(state));
const searchParams = `?state=${stateStr}`;
const res = await fetch("/api/integrations/" + type.replace("_", "") + "/add" + searchParams);
if (!res.ok) {
throw new Error("Something went wrong");
}
const json = await res.json();
window.location.href = json.url;
setIsLoading(true);
});
const [isModalOpen, _setIsModalOpen] = useState(false);
const setIsModalOpen = (v: boolean) => {
_setIsModalOpen(v);
props.onOpenChange(v);
};
return (
<>
{props.render({
onClick() {
if (["caldav_calendar", "apple_calendar"].includes(type)) {
// special handlers
setIsModalOpen(true);
return;
}
mutation.mutate();
},
loading: mutation.isLoading || isLoading,
disabled: isModalOpen,
})}
{type === "caldav_calendar" && (
<AddCalDavIntegrationModal open={isModalOpen} onOpenChange={setIsModalOpen} />
)}
{type === "apple_calendar" && (
<AddAppleIntegrationModal open={isModalOpen} onOpenChange={setIsModalOpen} />
)}
</>
);
}

View File

@@ -0,0 +1,60 @@
import { useState } from "react";
import { useMutation } from "react-query";
import { Dialog } from "@components/Dialog";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import { ButtonBaseProps } from "@components/ui/Button";
export default function DisconnectIntegration(props: {
/** Integration credential id */
id: number;
render: (renderProps: ButtonBaseProps) => JSX.Element;
onOpenChange: (isOpen: boolean) => unknown | Promise<unknown>;
}) {
const [modalOpen, setModalOpen] = useState(false);
const mutation = useMutation(
async () => {
const res = await fetch("/api/integrations", {
method: "DELETE",
body: JSON.stringify({ id: props.id }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) {
throw new Error("Something went wrong");
}
},
{
async onSettled() {
await props.onOpenChange(modalOpen);
},
onSuccess() {
setModalOpen(false);
},
}
);
return (
<>
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<ConfirmationDialogContent
variety="danger"
title="Disconnect Integration"
confirmBtnText="Yes, disconnect integration"
cancelBtnText="Cancel"
onConfirm={() => {
mutation.mutate();
}}>
Are you sure you want to disconnect this integration?
</ConfirmationDialogContent>
</Dialog>
{props.render({
onClick() {
setModalOpen(true);
},
disabled: modalOpen,
loading: mutation.isLoading,
})}
</>
);
}

View File

@@ -0,0 +1,30 @@
import Image from "next/image";
import { ReactNode } from "react";
import classNames from "@lib/classNames";
import { ListItem, ListItemText, ListItemTitle } from "@components/List";
function IntegrationListItem(props: {
imageSrc: string;
title: string;
description: string;
actions?: ReactNode;
children?: ReactNode;
}): JSX.Element {
return (
<ListItem expanded={!!props.children} className={classNames("flex-col")}>
<div className={classNames("flex flex-1 space-x-2 w-full p-3 items-center")}>
<Image width={40} height={40} src={`/${props.imageSrc}`} alt={props.title} />
<div className="flex-grow pl-2 truncate">
<ListItemTitle component="h3">{props.title}</ListItemTitle>
<ListItemText component="p">{props.description}</ListItemText>
</div>
<div>{props.actions}</div>
</div>
{props.children && <div className="w-full border-t border-gray-200">{props.children}</div>}
</ListItem>
);
}
export default IntegrationListItem;

View File

@@ -0,0 +1,29 @@
import { ReactNode } from "react";
import Badge from "@components/ui/Badge";
function pluralize(opts: { num: number; plural: string; singular: string }) {
if (opts.num === 0) {
return opts.singular;
}
return opts.singular;
}
export default function SubHeadingTitleWithConnections(props: { title: ReactNode; numConnections?: number }) {
const num = props.numConnections;
return (
<>
<span>{props.title}</span>
{num ? (
<Badge variant="success">
{num}{" "}
{pluralize({
num,
singular: "connection",
plural: "connections",
})}
</Badge>
) : null}
</>
);
}

View File

@@ -0,0 +1,128 @@
import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client";
import React, { FC } from "react";
import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form";
import Select, { OptionTypeBase } from "react-select";
import { useLocale } from "@lib/hooks/useLocale";
interface Props {
onSubmit: SubmitHandler<IFormInput>;
onCancel: () => void;
selectedCustomInput?: EventTypeCustomInput;
}
type IFormInput = EventTypeCustomInput;
const CustomInputTypeForm: FC<Props> = (props) => {
const { t } = useLocale();
const inputOptions: OptionTypeBase[] = [
{ value: EventTypeCustomInputType.TEXT, label: t("text") },
{ value: EventTypeCustomInputType.TEXTLONG, label: t("multiline_text") },
{ value: EventTypeCustomInputType.NUMBER, label: t("number") },
{ value: EventTypeCustomInputType.BOOL, label: t("checkbox") },
];
const { selectedCustomInput } = props;
const defaultValues = selectedCustomInput || { type: inputOptions[0].value };
const { register, control, handleSubmit } = useForm<IFormInput>({
defaultValues,
});
const selectedInputType = useWatch({ name: "type", control });
const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value)!;
const onCancel = () => {
props.onCancel();
};
return (
<form onSubmit={handleSubmit(props.onSubmit)}>
<div className="mb-2">
<label htmlFor="type" className="block text-sm font-medium text-gray-700">
{t("input_type")}
</label>
<Controller
name="type"
control={control}
render={({ field }) => (
<Select
id="type"
defaultValue={selectedInputOption}
options={inputOptions}
isSearchable={false}
className="flex-1 block w-full min-w-0 mt-1 mb-2 border-gray-300 rounded-none focus:ring-primary-500 focus:border-primary-500 rounded-r-md sm:text-sm"
onChange={(option) => field.onChange(option.value)}
value={selectedInputOption}
onBlur={field.onBlur}
name={field.name}
/>
)}
/>
</div>
<div className="mb-2">
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
{t("label")}
</label>
<div className="mt-1">
<input
type="text"
id="label"
required
className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
defaultValue={selectedCustomInput?.label}
{...register("label", { required: true })}
/>
</div>
</div>
{(selectedInputType === EventTypeCustomInputType.TEXT ||
selectedInputType === EventTypeCustomInputType.TEXTLONG) && (
<div className="mb-2">
<label htmlFor="placeholder" className="block text-sm font-medium text-gray-700">
{t("placeholder")}
</label>
<div className="mt-1">
<input
type="text"
id="placeholder"
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-sm"
defaultValue={selectedCustomInput?.placeholder}
{...register("placeholder")}
/>
</div>
</div>
)}
<div className="flex items-center h-5">
<input
id="required"
type="checkbox"
className="w-4 h-4 mr-2 border-gray-300 rounded focus:ring-primary-500 text-primary-600"
defaultChecked={selectedCustomInput?.required ?? true}
{...register("required")}
/>
<label htmlFor="required" className="block text-sm font-medium text-gray-700">
{t("is_required")}
</label>
</div>
<input
type="hidden"
id="eventTypeId"
value={selectedCustomInput?.eventTypeId || -1}
{...register("eventTypeId", { valueAsNumber: true })}
/>
<input
type="hidden"
id="id"
value={selectedCustomInput?.id || -1}
{...register("id", { valueAsNumber: true })}
/>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="submit" className="btn btn-primary">
{t("save")}
</button>
<button onClick={onCancel} type="button" className="mr-2 btn btn-white">
{t("cancel")}
</button>
</div>
</form>
);
};
export default CustomInputTypeForm;

View File

@@ -1,22 +1,19 @@
import React, { SyntheticEvent, useState } from "react";
import Modal from "@components/Modal";
import { ErrorCode } from "@lib/auth";
const errorMessages: { [key: string]: string } = {
[ErrorCode.IncorrectPassword]: "Current password is incorrect",
[ErrorCode.NewPasswordMatchesOld]:
"New password matches your old password. Please choose a different password.",
};
import { ErrorCode } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
const ChangePasswordSection = () => {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [successModalOpen, setSuccessModalOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const { t } = useLocale();
const closeSuccessModal = () => {
setSuccessModalOpen(false);
const errorMessages: { [key: string]: string } = {
[ErrorCode.IncorrectPassword]: t("current_incorrect_password"),
[ErrorCode.NewPasswordMatchesOld]: t("new_password_matches_old_password"),
};
async function changePasswordHandler(e: SyntheticEvent) {
@@ -41,15 +38,15 @@ const ChangePasswordSection = () => {
if (response.status === 200) {
setOldPassword("");
setNewPassword("");
setSuccessModalOpen(true);
showToast(t("password_has_been_changed"), "success");
return;
}
const body = await response.json();
setErrorMessage(errorMessages[body.error] || "Something went wrong. Please try again");
setErrorMessage(errorMessages[body.error] || `${t("something_went_wrong")}${t("please_try_again")}`);
} catch (err) {
console.error("Error changing password", err);
setErrorMessage("Something went wrong. Please try again");
console.error(t("error_changing_password"), err);
setErrorMessage(`${t("something_went_wrong")}${t("please_try_again")}`);
} finally {
setIsSubmitting(false);
}
@@ -58,14 +55,14 @@ const ChangePasswordSection = () => {
return (
<>
<div className="mt-6">
<h2 className="text-lg leading-6 font-medium text-gray-900">Change Password</h2>
<h2 className="font-cal text-lg leading-6 font-medium 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="flex">
<div className="w-1/2 mr-2">
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
Current Password
{t("current_password")}
</label>
<div className="mt-1">
<input
@@ -75,14 +72,14 @@ const ChangePasswordSection = () => {
name="current_password"
id="current_password"
required
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="Your old password"
className="shadow-sm focus:ring-black focus:border-brand block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder={t("your_old_password")}
/>
</div>
</div>
<div className="w-1/2 ml-2">
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
New Password
{t("new_password")}
</label>
<div className="mt-1">
<input
@@ -92,8 +89,8 @@ const ChangePasswordSection = () => {
value={newPassword}
required
onInput={(e) => setNewPassword(e.currentTarget.value)}
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder="Your super secure new password"
className="shadow-sm focus:ring-black focus:border-brand block w-full sm:text-sm border-gray-300 rounded-sm"
placeholder={t("super_secure_new_password")}
/>
</div>
</div>
@@ -103,18 +100,12 @@ const ChangePasswordSection = () => {
<button
type="submit"
className="ml-2 bg-neutral-900 border border-transparent rounded-sm shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
Save
{t("save")}
</button>
</div>
<hr className="mt-4" />
</div>
</form>
<Modal
heading="Password updated successfully"
description="Your password has been successfully changed."
open={successModalOpen}
handleClose={closeSuccessModal}
/>
</>
);
};

View File

@@ -1,19 +1,18 @@
import { SyntheticEvent, useState } from "react";
import Button from "@components/ui/Button";
import { Dialog, DialogContent } from "@components/Dialog";
import { ErrorCode } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import { Dialog, DialogContent } from "@components/Dialog";
import Button from "@components/ui/Button";
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
import TwoFactorModalHeader from "./TwoFactorModalHeader";
interface DisableTwoFactorAuthModalProps {
/**
* Called when the user closes the modal without disabling two-factor auth
*/
/** Called when the user closes the modal without disabling two-factor auth */
onCancel: () => void;
/**
* Called when the user disables two-factor auth
*/
/** Called when the user disables two-factor auth */
onDisable: () => void;
}
@@ -21,6 +20,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
const [password, setPassword] = useState("");
const [isDisabling, setIsDisabling] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const { t } = useLocale();
async function handleDisable(e: SyntheticEvent) {
e.preventDefault();
@@ -40,13 +40,13 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
const body = await response.json();
if (body.error === ErrorCode.IncorrectPassword) {
setErrorMessage("Password is incorrect.");
setErrorMessage(t("incorrect_password"));
} else {
setErrorMessage("Something went wrong.");
setErrorMessage(t("something_went_wrong"));
}
} catch (e) {
setErrorMessage("Something went wrong.");
console.error("Error disabling two-factor authentication", e);
setErrorMessage(t("something_went_wrong"));
console.error(t("error_disabling_2fa"), e);
} finally {
setIsDisabling(false);
}
@@ -55,15 +55,12 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
return (
<Dialog open={true}>
<DialogContent>
<TwoFactorModalHeader
title="Disable two-factor authentication"
description="If you need to disable 2FA, we recommend re-enabling it as soon as possible."
/>
<TwoFactorModalHeader title={t("disable_2fa")} description={t("disable_2fa_recommendation")} />
<form onSubmit={handleDisable}>
<div className="mb-4">
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
Password
{t("password")}
</label>
<div className="mt-1">
<input
@@ -87,10 +84,10 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
className="ml-2"
onClick={handleDisable}
disabled={password.length === 0 || isDisabling}>
Disable
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>
Cancel
{t("cancel")}
</Button>
</div>
</DialogContent>

View File

@@ -1,7 +1,11 @@
import React, { SyntheticEvent, useState } from "react";
import Button from "@components/ui/Button";
import { Dialog, DialogContent } from "@components/Dialog";
import { ErrorCode } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import { Dialog, DialogContent } from "@components/Dialog";
import Button from "@components/ui/Button";
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
import TwoFactorModalHeader from "./TwoFactorModalHeader";
@@ -23,12 +27,6 @@ enum SetupStep {
EnterTotpCode,
}
const setupDescriptions = {
[SetupStep.ConfirmPassword]: "Confirm your current password to get started.",
[SetupStep.DisplayQrCode]: "Scan the image below with the authenticator app on your phone.",
[SetupStep.EnterTotpCode]: "Enter the six-digit code from your authenticator app below.",
};
const WithStep = ({
step,
current,
@@ -42,10 +40,17 @@ const WithStep = ({
};
const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps) => {
const { t } = useLocale();
const setupDescriptions = {
[SetupStep.ConfirmPassword]: t("2fa_confirm_current_password"),
[SetupStep.DisplayQrCode]: t("2fa_scan_image_or_use_code"),
[SetupStep.EnterTotpCode]: t("2fa_enter_six_digit_code"),
};
const [step, setStep] = useState(SetupStep.ConfirmPassword);
const [password, setPassword] = useState("");
const [totpCode, setTotpCode] = useState("");
const [dataUri, setDataUri] = useState("");
const [secret, setSecret] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
@@ -65,18 +70,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
if (response.status === 200) {
setDataUri(body.dataUri);
setSecret(body.secret);
setStep(SetupStep.DisplayQrCode);
return;
}
if (body.error === ErrorCode.IncorrectPassword) {
setErrorMessage("Password is incorrect.");
setErrorMessage(t("incorrect_password"));
} else {
setErrorMessage("Something went wrong.");
setErrorMessage(t("something_went_wrong"));
}
} catch (e) {
setErrorMessage("Something went wrong.");
console.error("Error setting up two-factor authentication", e);
setErrorMessage(t("something_went_wrong"));
console.error(t("error_enabling_2fa"), e);
} finally {
setIsSubmitting(false);
}
@@ -102,13 +108,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
}
if (body.error === ErrorCode.IncorrectTwoFactorCode) {
setErrorMessage("Code is incorrect. Please try again.");
setErrorMessage(`${t("code_is_incorrect")} ${t("please_try_again")}`);
} else {
setErrorMessage("Something went wrong.");
setErrorMessage(t("something_went_wrong"));
}
} catch (e) {
setErrorMessage("Something went wrong.");
console.error("Error enabling up two-factor authentication", e);
setErrorMessage(t("something_went_wrong"));
console.error(t("error_enabling_2fa"), e);
} finally {
setIsSubmitting(false);
}
@@ -117,16 +123,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
return (
<Dialog open={true}>
<DialogContent>
<TwoFactorModalHeader
title="Enable two-factor authentication"
description={setupDescriptions[step]}
/>
<TwoFactorModalHeader title={t("enable_2fa")} description={setupDescriptions[step]} />
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<form onSubmit={handleSetup}>
<div className="mb-4">
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
Password
{t("password")}
</label>
<div className="mt-1">
<input
@@ -145,15 +148,18 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
</form>
</WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<div className="flex justify-center">
<img src={dataUri} />
</div>
<>
<div className="flex justify-center">
<img src={dataUri} />
</div>
<p className="text-center text-xs font-mono">{secret}</p>
</>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<form onSubmit={handleEnable}>
<div className="mb-4">
<label htmlFor="code" className="mt-4 block text-sm font-medium text-gray-700">
Code
{t("code")}
</label>
<div className="mt-1">
<input
@@ -182,12 +188,12 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
className="ml-2"
onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}>
Continue
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button type="submit" className="ml-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
Continue
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
@@ -196,11 +202,11 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
className="ml-2"
onClick={handleEnable}
disabled={totpCode.length !== 6 || isSubmitting}>
Enable
{t("enable")}
</Button>
</WithStep>
<Button color="secondary" onClick={onCancel}>
Cancel
{t("cancel")}
</Button>
</div>
</DialogContent>

View File

@@ -1,31 +1,34 @@
import { useState } from "react";
import Button from "@components/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import Badge from "@components/ui/Badge";
import EnableTwoFactorModal from "./EnableTwoFactorModal";
import Button from "@components/ui/Button";
import DisableTwoFactorModal from "./DisableTwoFactorModal";
import EnableTwoFactorModal from "./EnableTwoFactorModal";
const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean }) => {
const [enabled, setEnabled] = useState(twoFactorEnabled);
const [enableModalOpen, setEnableModalOpen] = useState(false);
const [disableModalOpen, setDisableModalOpen] = useState(false);
const { t } = useLocale();
return (
<>
<div className="flex flex-row items-center">
<h2 className="text-lg leading-6 font-medium text-gray-900">Two-Factor Authentication</h2>
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">{t("2fa")}</h2>
<Badge className="text-xs ml-2" variant={enabled ? "success" : "gray"}>
{enabled ? "Enabled" : "Disabled"}
{enabled ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="mt-1 text-sm text-gray-500">
Add an extra layer of security to your account in case your password is stolen.
</p>
<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 ? "Disable" : "Enable"} Two-Factor Authentication
{enabled ? t("disable") : t("enable")} {t("2fa")}
</Button>
{enableModalOpen && (

View File

@@ -1,14 +1,14 @@
import React from "react";
import { ShieldCheckIcon } from "@heroicons/react/solid";
import React from "react";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-black rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-brand rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<ShieldCheckIcon className="w-6 h-6 text-black" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
{title}
</h3>
<p className="text-sm text-gray-400">{description}</p>

View File

@@ -1,8 +1,9 @@
import merge from "lodash/merge";
import { NextSeo, NextSeoProps } from "next-seo";
import React from "react";
import { getBrowserInfo } from "@lib/core/browser/browser.utils";
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
import merge from "lodash.merge";
import { getBrowserInfo } from "@lib/core/browser/browser.utils";
export type HeadSeoProps = {
title: string;
@@ -68,7 +69,7 @@ const buildSeoMeta = (pageProps: {
const constructImage = (name: string, avatar: string, description: string): string => {
return (
encodeURIComponent("Meet **" + name + "** <br>" + description).replace(/'/g, "%27") +
".png?md=1&images=https%3A%2F%2Fcal.com%2Fcalendso-logo-white.svg&images=" +
".png?md=1&images=https%3A%2F%2Fcal.com%2Flogo-white.svg&images=" +
encodeURIComponent(avatar)
);
};

View File

@@ -1,18 +1,22 @@
import React, { useEffect, useRef, useState } from "react";
import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
import ErrorAlert from "@components/ui/alerts/Error";
import { UsernameInput } from "@components/ui/UsernameInput";
import MemberList from "./MemberList";
import Avatar from "@components/ui/Avatar";
import ImageUploader from "@components/ImageUploader";
import { Dialog, DialogTrigger } from "@components/Dialog";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Modal from "@components/Modal";
import MemberInvitationModal from "@components/team/MemberInvitationModal";
import Button from "@components/ui/Button";
import React, { useEffect, useRef, useState } from "react";
import { useLocale } from "@lib/hooks/useLocale";
import { Member } from "@lib/member";
import showToast from "@lib/notification";
import { Team } from "@lib/team";
import { Dialog, DialogTrigger } from "@components/Dialog";
import ImageUploader from "@components/ImageUploader";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import MemberInvitationModal from "@components/team/MemberInvitationModal";
import Avatar from "@components/ui/Avatar";
import Button from "@components/ui/Button";
import { UsernameInput } from "@components/ui/UsernameInput";
import ErrorAlert from "@components/ui/alerts/Error";
import MemberList from "./MemberList";
export default function EditTeam(props: { team: Team | undefined | null; onCloseEdit: () => void }) {
const [members, setMembers] = useState([]);
@@ -22,11 +26,11 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
const hideBrandingRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const logoRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
const [hasErrors, setHasErrors] = useState(false);
const [successModalOpen, setSuccessModalOpen] = useState(false);
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false);
const [inviteModalTeam, setInviteModalTeam] = useState<Team | null | undefined>();
const [errorMessage, setErrorMessage] = useState("");
const [imageSrc, setImageSrc] = useState<string>("");
const { t } = useLocale();
const loadMembers = () =>
fetch("/api/teams/" + props.team?.id + "/membership")
@@ -91,7 +95,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
})
.then(handleError)
.then(() => {
setSuccessModalOpen(true);
showToast(t("your_team_updated_successfully"), "success");
setHasErrors(false); // dismiss any open errors
})
.catch((err) => {
@@ -105,10 +109,6 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
setShowMemberInvitationModal(false);
};
const closeSuccessModal = () => {
setSuccessModalOpen(false);
};
const handleLogoChange = (newLogo: string) => {
logoRef.current.value = newLogo;
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement?.prototype, "value").set;
@@ -129,19 +129,19 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
size="sm"
StartIcon={ArrowLeftIcon}
onClick={() => props.onCloseEdit()}>
Back
{t("back")}
</Button>
</div>
<div className="">
<div>
<div className="pb-5 pr-4 sm:pb-6">
<h3 className="text-lg font-bold leading-6 text-gray-900">{props.team?.name}</h3>
<div className="max-w-xl mt-2 text-sm text-gray-500">
<p>Manage your team</p>
<p>{t("manage_your_team")}</p>
</div>
</div>
</div>
<hr className="mt-2" />
<h3 className="font-bold leading-6 text-gray-900 mt-7 text-md">Profile</h3>
<h3 className="font-cal font-bold leading-6 text-gray-900 mt-7 text-md">{t("profile")}</h3>
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateTeamHandler}>
{hasErrors && <ErrorAlert message={errorMessage} />}
<div className="py-6 lg:pb-8">
@@ -149,18 +149,22 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
<div className="flex-grow space-y-6">
<div className="block sm:flex">
<div className="w-full mb-6 sm:w-1/2 sm:mr-2">
<UsernameInput ref={teamUrlRef} defaultValue={props.team?.slug} label={"My team URL"} />
<UsernameInput
ref={teamUrlRef}
defaultValue={props.team?.slug}
label={t("my_team_url")}
/>
</div>
<div className="w-full sm:w-1/2 sm:ml-2">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Team name
{t("team_name")}
</label>
<input
ref={nameRef}
type="text"
name="name"
id="name"
placeholder="Your team name"
placeholder={t("your_team_name")}
required
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
defaultValue={props.team?.name}
@@ -169,7 +173,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
</div>
<div>
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
About
{t("about")}
</label>
<div className="mt-1">
<textarea
@@ -179,9 +183,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
rows={3}
defaultValue={props.team?.bio}
className="block w-full mt-1 border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"></textarea>
<p className="mt-2 text-sm text-gray-500">
A few sentences about your team. This will appear on your team&apos;s URL page.
</p>
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
</div>
</div>
<div>
@@ -198,27 +200,27 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
id="avatar"
placeholder="URL"
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
defaultValue={imageSrc ? imageSrc : props.team?.logo}
defaultValue={imageSrc ?? props.team?.logo}
/>
<ImageUploader
target="logo"
id="logo-upload"
buttonMsg={imageSrc !== "" ? "Edit logo" : "Upload a logo"}
buttonMsg={imageSrc !== "" ? t("edit_logo") : t("upload_a_logo")}
handleAvatarChange={handleLogoChange}
imageRef={imageSrc ? imageSrc : props.team?.logo}
imageSrc={imageSrc ?? props.team?.logo}
/>
</div>
<hr className="mt-6" />
</div>
<div className="flex justify-between mt-7">
<h3 className="font-bold leading-6 text-gray-900 text-md">Members</h3>
<h3 className="font-cal font-bold leading-6 text-gray-900 text-md">{t("members")}</h3>
<div className="relative flex items-center">
<Button
type="button"
color="secondary"
StartIcon={PlusIcon}
onClick={() => onInviteMember(props.team)}>
New Member
{t("new_member")}
</Button>
</div>
</div>
@@ -242,14 +244,14 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
</div>
<div className="ml-3 text-sm">
<label htmlFor="hide-branding" className="font-medium text-gray-700">
Disable Cal.com branding
{t("disable_cal_branding")}
</label>
<p className="text-gray-500">Hide all Cal.com branding from your public pages.</p>
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
</div>
</div>
<hr className="mt-6" />
</div>
<h3 className="font-bold leading-6 text-gray-900 mt-7 text-md">Danger Zone</h3>
<h3 className="font-bold leading-6 text-gray-900 mt-7 text-md">{t("danger_zone")}</h3>
<div>
<div className="relative flex items-start">
<Dialog>
@@ -259,16 +261,14 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
}}
className="btn-sm btn-white">
<TrashIcon className="group-hover:text-red text-gray-700 w-3.5 h-3.5 mr-2 inline-block" />
Disband Team
{t("disband_team")}
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title="Disband Team"
confirmBtnText="Yes, disband team"
cancelBtnText="Cancel"
title={t("disband_team")}
confirmBtnText={t("confirm_disband_team")}
onConfirm={() => deleteTeam()}>
Are you sure you want to disband this team? Anyone who you&apos;ve shared this team
link with will no longer be able to book using it.
{t("disband_team_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
</div>
@@ -278,17 +278,11 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
<hr className="mt-8" />
<div className="flex justify-end py-4">
<Button type="submit" color="primary">
Save
{t("save")}
</Button>
</div>
</div>
</form>
<Modal
heading="Team updated successfully"
description="Your team has been updated successfully."
open={successModalOpen}
handleClose={closeSuccessModal}
/>
{showMemberInvitationModal && (
<MemberInvitationModal team={inviteModalTeam} onExit={onMemberInvitationModalExit} />
)}

View File

@@ -1,10 +1,15 @@
import { UsersIcon } from "@heroicons/react/outline";
import { useState } from "react";
import Button from "@components/ui/Button";
import React, { SyntheticEvent } from "react";
import { useLocale } from "@lib/hooks/useLocale";
import { Team } from "@lib/team";
import Button from "@components/ui/Button";
export default function MemberInvitationModal(props: { team: Team | undefined | null; onExit: () => void }) {
const [errorMessage, setErrorMessage] = useState("");
const { t, i18n } = useLocale();
const handleError = async (res: Response) => {
const responseData = await res.json();
@@ -17,13 +22,22 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
return responseData;
};
const inviteMember = (e) => {
const inviteMember = (e: SyntheticEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
elements: {
role: { value: string };
inviteUser: { value: string };
sendInviteEmail: { checked: boolean };
};
};
const payload = {
role: e.target.elements["role"].value,
usernameOrEmail: e.target.elements["inviteUser"].value,
sendEmailInvitation: e.target.elements["sendInviteEmail"].checked,
language: i18n.language,
role: target.elements["role"].value,
usernameOrEmail: target.elements["inviteUser"].value,
sendEmailInvitation: target.elements["sendInviteEmail"].checked,
};
return fetch("/api/teams/" + props?.team?.id + "/invite", {
@@ -57,15 +71,15 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
<div className="inline-block px-4 pt-5 pb-4 text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
<div className="mb-4 sm:flex sm:items-start">
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-black rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<div className="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-brand rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<UsersIcon className="w-6 h-6 text-black" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
Invite a new member
{t("invite_new_member")}
</h3>
<div>
<p className="text-sm text-gray-400">Invite someone to your team.</p>
<p className="text-sm text-gray-400">{t("invite_new_team_member")}</p>
</div>
</div>
</div>
@@ -73,7 +87,7 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
<div>
<div className="mb-4">
<label htmlFor="inviteUser" className="block text-sm font-medium text-gray-700">
Email or Username
{t("email_or_username")}
</label>
<input
type="text"
@@ -81,18 +95,18 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
id="inviteUser"
placeholder="email@example.com"
required
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-black focus:border-black sm:text-sm"
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-black focus:border-brand sm:text-sm"
/>
</div>
<div className="mb-4">
<label className="block mb-2 text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
Role
{t("role")}
</label>
<select
id="role"
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-black focus:border-black sm:text-sm">
<option value="MEMBER">Member</option>
<option value="OWNER">Owner</option>
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-black focus:border-brand sm:text-sm">
<option value="MEMBER">{t("member")}</option>
<option value="OWNER">{t("owner")}</option>
</select>
</div>
<div className="relative flex items-start">
@@ -102,12 +116,12 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
name="sendInviteEmail"
defaultChecked
id="sendInviteEmail"
className="text-black border-gray-300 rounded-md shadow-sm focus:ring-black focus:border-black sm:text-sm"
className="text-black border-gray-300 rounded-md shadow-sm focus:ring-black focus:border-brand sm:text-sm"
/>
</div>
<div className="ml-2 text-sm">
<label htmlFor="sendInviteEmail" className="font-medium text-gray-700">
Send an invite email
{t("send_invite_email")}
</label>
</div>
</div>
@@ -120,10 +134,10 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
)}
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" color="primary" className="ml-2">
Invite
{t("invite")}
</Button>
<Button type="button" color="secondary" onClick={props.onExit}>
Cancel
{t("cancel")}
</Button>
</div>
</form>

View File

@@ -1,6 +1,7 @@
import MemberListItem from "./MemberListItem";
import { Member } from "@lib/member";
import MemberListItem from "./MemberListItem";
export default function MemberList(props: {
members: Member[];
onRemoveMember: (text: Member) => void;
@@ -16,7 +17,7 @@ export default function MemberList(props: {
return (
<div>
<ul className="px-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
<ul className="px-6 mb-2 -mx-6 bg-white border divide-y divide-gray-200 rounded sm:px-4 sm:mx-0">
{props.members.map((member) => (
<MemberListItem
onChange={props.onChange}

View File

@@ -1,60 +1,71 @@
import { DotsHorizontalIcon, UserRemoveIcon } from "@heroicons/react/outline";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/Dropdown";
import { useState } from "react";
import { useLocale } from "@lib/hooks/useLocale";
import { Member } from "@lib/member";
import { Dialog, DialogTrigger } from "@components/Dialog";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";
import { Member } from "@lib/member";
import Button from "@components/ui/Button";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/Dropdown";
export default function MemberListItem(props: {
member: Member;
onActionSelect: (text: string) => void;
onChange: (text: string) => void;
}) {
const [member] = useState(props.member);
const { t } = useLocale();
return (
member && (
<li className="divide-y">
<div className="flex justify-between my-4">
<div className="flex">
<Avatar
imageSrc={
props.member.avatar
? props.member.avatar
: "https://eu.ui-avatars.com/api/?background=fff&color=039be5&name=" +
encodeURIComponent(props.member.name || "")
}
displayName={props.member.name || ""}
className="rounded-full w-9 h-9"
/>
<div className="inline-block ml-3">
<span className="text-sm font-bold text-neutral-700">{props.member.name}</span>
<span className="block -mt-1 text-xs text-gray-400">{props.member.email}</span>
<div className="flex flex-col justify-between w-full sm:flex-row">
<div className="flex">
<Avatar
imageSrc={
props.member.avatar
? props.member.avatar
: "https://eu.ui-avatars.com/api/?background=fff&color=039be5&name=" +
encodeURIComponent(props.member.name || "")
}
alt={props.member.name || ""}
className="rounded-full w-9 h-9"
/>
<div className="inline-block ml-3">
<span className="text-sm font-bold text-neutral-700">{props.member.name}</span>
<span className="block -mt-1 text-xs text-gray-400">{props.member.email}</span>
</div>
</div>
<div>
{props.member.role === "INVITEE" && (
<>
<span className="self-center h-6 px-3 py-1 mr-2 text-xs text-yellow-700 capitalize rounded-md bg-yellow-50">
{t("pending")}
</span>
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
{t("member")}
</span>
</>
)}
{props.member.role === "MEMBER" && (
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
{t("member")}
</span>
)}
{props.member.role === "OWNER" && (
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-blue-700 capitalize rounded-md bg-blue-50">
{t("owner")}
</span>
)}
</div>
</div>
<div className="flex">
{props.member.role === "INVITEE" && (
<>
<span className="self-center h-6 px-3 py-1 mr-2 text-xs text-yellow-700 capitalize rounded-md bg-yellow-50">
Pending
</span>
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
Member
</span>
</>
)}
{props.member.role === "MEMBER" && (
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
Member
</span>
)}
{props.member.role === "OWNER" && (
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-blue-700 capitalize rounded-md bg-blue-50">
Owner
</span>
)}
{/* <div className="flex flex-col-reverse"> */}
<Dropdown>
<DropdownMenuTrigger>
<DotsHorizontalIcon className="w-5 h-5" />
@@ -70,21 +81,21 @@ export default function MemberListItem(props: {
color="warn"
StartIcon={UserRemoveIcon}
className="w-full">
Remove User
{t("remove_member")}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title="Remove member"
confirmBtnText="Yes, remove member"
cancelBtnText="Cancel"
title={t("remove_member")}
confirmBtnText={t("confirm_remove_member")}
onConfirm={() => props.onActionSelect("remove")}>
Are you sure you want to remove this member from the team?
{t("remove_member_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
{/* </div> */}
</div>
</div>
</li>

View File

@@ -1,6 +1,7 @@
import TeamListItem from "./TeamListItem";
import { Team } from "@lib/team";
import TeamListItem from "./TeamListItem";
export default function TeamList(props: {
teams: Team[];
onChange: () => void;
@@ -17,10 +18,11 @@ export default function TeamList(props: {
}
};
const deleteTeam = (team: Team) => {
return fetch("/api/teams/" + team.id, {
const deleteTeam = async (team: Team) => {
await fetch("/api/teams/" + team.id, {
method: "DELETE",
}).then(props.onChange());
});
return props.onChange();
};
return (

View File

@@ -5,15 +5,19 @@ import {
PencilAltIcon,
TrashIcon,
} from "@heroicons/react/outline";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/Dropdown";
import { useState } from "react";
import { Tooltip } from "@components/Tooltip";
import Link from "next/link";
import { useState } from "react";
import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { Dialog, DialogTrigger } from "@components/Dialog";
import { Tooltip } from "@components/Tooltip";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";
import Button from "@components/ui/Button";
import showToast from "@lib/notification";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/Dropdown";
interface Team {
id: number;
@@ -33,6 +37,7 @@ export default function TeamListItem(props: {
onActionSelect: (text: string) => void;
}) {
const [team, setTeam] = useState<Team | null>(props.team);
const { t } = useLocale();
const acceptInvite = () => invitationResponse(true);
const declineInvite = () => invitationResponse(false);
@@ -56,57 +61,61 @@ export default function TeamListItem(props: {
<div className="flex justify-between my-4">
<div className="flex">
<Avatar
size={9}
imageSrc={
props.team.logo
? props.team.logo
: "https://eu.ui-avatars.com/api/?background=fff&color=039be5&name=" +
encodeURIComponent(props.team.name || "")
}
displayName="Team Logo"
alt="Team Logo"
className="rounded-full w-9 h-9"
/>
<div className="inline-block ml-3">
<span className="text-sm font-bold text-neutral-700">{props.team.name}</span>
<span className="block -mt-1 text-xs text-gray-400">
{window.location.hostname}/{props.team.slug}
{process.env.NEXT_PUBLIC_APP_URL}/team/{props.team.slug}
</span>
</div>
</div>
{props.team.role === "INVITEE" && (
<div>
<Button type="button" color="secondary" onClick={declineInvite}>
Reject
{t("reject")}
</Button>
<Button type="button" color="primary" className="ml-1" onClick={acceptInvite}>
Accept
{t("accept")}
</Button>
</div>
)}
{props.team.role === "MEMBER" && (
<div>
<Button type="button" color="primary" onClick={declineInvite}>
Leave
{t("leave")}
</Button>
</div>
)}
{props.team.role === "OWNER" && (
<div className="flex">
<div className="flex space-x-4">
<span className="self-center h-6 px-3 py-1 text-xs text-gray-700 capitalize rounded-md bg-gray-50">
Owner
{t("owner")}
</span>
<Tooltip content="Copy link">
<Tooltip content={t("copy_link")}>
<Button
onClick={() => {
navigator.clipboard.writeText(window.location.hostname + "/team/" + props.team.slug);
showToast("Link copied!", "success");
navigator.clipboard.writeText(
process.env.NEXT_PUBLIC_APP_URL + "/team/" + props.team.slug
);
showToast(t("link_copied"), "success");
}}
size="icon"
color="minimal"
className="w-full pl-5 ml-8"
StartIcon={LinkIcon}
type="button"></Button>
type="button"
/>
</Tooltip>
<Dropdown>
<DropdownMenuTrigger>
<DropdownMenuTrigger className="group w-10 h-10 p-0 border border-transparent text-neutral-400 hover:border-gray-200">
<DotsHorizontalIcon className="w-5 h-5" />
</DropdownMenuTrigger>
<DropdownMenuContent>
@@ -118,15 +127,15 @@ export default function TeamListItem(props: {
onClick={() => props.onActionSelect("edit")}
StartIcon={PencilAltIcon}>
{" "}
Edit team
{t("edit_team")}
</Button>
</DropdownMenuItem>
<DropdownMenuItem className="">
<Link href={`/team/${props.team.slug}`} passHref={true}>
<DropdownMenuItem>
<Link href={`${process.env.NEXT_PUBLIC_APP_URL}/team/${props.team.slug}`} passHref={true}>
<a target="_blank">
<Button type="button" color="minimal" className="w-full" StartIcon={ExternalLinkIcon}>
{" "}
Preview team page
{t("preview_team")}
</Button>
</a>
</Link>
@@ -141,17 +150,15 @@ export default function TeamListItem(props: {
color="warn"
StartIcon={TrashIcon}
className="w-full">
Disband Team
{t("disband_team")}
</Button>
</DialogTrigger>
<ConfirmationDialogContent
variety="danger"
title="Disband Team"
confirmBtnText="Yes, disband team"
cancelBtnText="Cancel"
title={t("disband_team")}
confirmBtnText={t("confirm_disband_team")}
onConfirm={() => props.onActionSelect("disband")}>
Are you sure you want to disband this team? Anyone who you&apos;ve shared this team
link with will no longer be able to book using it.
{t("disband_team_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>
</DropdownMenuItem>

View File

@@ -1,13 +1,18 @@
import React from "react";
import Text from "@components/ui/Text";
import Link from "next/link";
import Avatar from "@components/ui/Avatar";
import { ArrowRightIcon } from "@heroicons/react/outline";
import classnames from "classnames";
import { ArrowLeftIcon } from "@heroicons/react/solid";
import classnames from "classnames";
import Link from "next/link";
import React from "react";
import { useLocale } from "@lib/hooks/useLocale";
import Avatar from "@components/ui/Avatar";
import Button from "@components/ui/Button";
import Text from "@components/ui/Text";
const Team = ({ team }) => {
const { t } = useLocale();
const Member = ({ member }) => {
const classes = classnames(
"group",
@@ -18,7 +23,7 @@ const Team = ({ team }) => {
"bg-white dark:bg-neutral-900 dark:border-0 dark:bg-opacity-8",
"border border-neutral-200",
"hover:cursor-pointer",
"hover:border-black dark:border-neutral-700 dark:hover:border-neutral-600",
"hover:border-brand dark:border-neutral-700 dark:hover:border-neutral-600",
"rounded-sm",
"hover:shadow-md"
);
@@ -56,9 +61,9 @@ const Team = ({ team }) => {
}
return (
<section className="mx-auto min-w-full lg:min-w-lg max-w-5xl grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-12 gap-y-6">
<section className="mx-auto min-w-full lg:min-w-lg max-w-5xl flex flex-wrap gap-x-12 gap-y-6 justify-center">
{members.map((member) => {
return <Member key={member.id} member={member} />;
return member.user.username !== null && <Member key={member.id} member={member} />;
})}
</section>
);
@@ -67,10 +72,10 @@ const Team = ({ team }) => {
return (
<div>
<Members members={team.members} />
{team.eventTypes.length && (
{team.eventTypes.length > 0 && (
<aside className="text-center dark:text-white mt-8">
<Button color="secondary" href={`/team/${team.slug}`} shallow={true} StartIcon={ArrowLeftIcon}>
Go back
{t("go_back")}
</Button>
</aside>
)}

View File

@@ -3,8 +3,9 @@ import classNames from "classnames";
import { ReactNode } from "react";
export interface AlertProps {
title: ReactNode;
title?: ReactNode;
message?: ReactNode;
actions?: ReactNode;
className?: string;
severity: "success" | "warning" | "error";
}
@@ -14,10 +15,10 @@ export function Alert(props: AlertProps) {
return (
<div
className={classNames(
"rounded-md p-4",
"rounded-sm p-2",
props.className,
severity === "error" && "bg-red-50 text-red-800",
severity === "warning" && "bg-yellow-50 text-yellow-800",
severity === "warning" && "bg-yellow-50 text-yellow-700",
severity === "success" && "bg-gray-900 text-white"
)}>
<div className="flex">
@@ -32,10 +33,11 @@ export function Alert(props: AlertProps) {
<CheckCircleIcon className={classNames("h-5 w-5 text-gray-400")} aria-hidden="true" />
)}
</div>
<div className="ml-3">
<div className="ml-3 flex-grow">
<h3 className="text-sm font-medium">{props.title}</h3>
<div className="text-sm">{props.message}</div>
</div>
{props.actions && <div className="text-sm">{props.actions}</div>}
</div>
</div>
);

View File

@@ -1,23 +1,27 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as Tooltip from "@radix-ui/react-tooltip";
import { defaultAvatarSrc } from "@lib/profile";
import classNames from "@lib/classNames";
import { defaultAvatarSrc } from "@lib/profile";
import { Maybe } from "@trpc/server";
export type AvatarProps = {
className?: string;
size: number;
imageSrc?: string;
size?: number;
imageSrc?: Maybe<string>;
title?: string;
alt: string;
gravatarFallbackMd5?: string;
};
export default function Avatar({ imageSrc, gravatarFallbackMd5, size, alt, title, ...props }: AvatarProps) {
const className = classNames("rounded-full", props.className, `h-${size} w-${size}`);
export default function Avatar(props: AvatarProps) {
const { imageSrc, gravatarFallbackMd5, size, alt, title } = props;
const className = classNames("rounded-full", props.className, size && `h-${size} w-${size}`);
const avatar = (
<AvatarPrimitive.Root>
<AvatarPrimitive.Image
src={imageSrc}
src={imageSrc ?? undefined}
alt={alt}
className={classNames("rounded-full", `h-auto w-${size}`, props.className)}
/>
@@ -32,7 +36,7 @@ export default function Avatar({ imageSrc, gravatarFallbackMd5, size, alt, title
return title ? (
<Tooltip.Tooltip delayDuration={300}>
<Tooltip.TooltipTrigger className="cursor-default">{avatar}</Tooltip.TooltipTrigger>
<Tooltip.Content className="p-2 rounded-sm text-sm bg-black text-white shadow-sm">
<Tooltip.Content className="p-2 rounded-sm text-sm bg-brand text-white shadow-sm">
<Tooltip.Arrow />
{title}
</Tooltip.Content>

View File

@@ -1,6 +1,9 @@
import React from "react";
import Avatar from "@components/ui/Avatar";
import classNames from "@lib/classNames";
import Avatar from "@components/ui/Avatar";
// import * as Tooltip from "@radix-ui/react-tooltip";
export type AvatarGroupProps = {
@@ -37,7 +40,7 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
<span className="w-16 absolute bottom-1.5 border-2 border-gray-300 flex-inline items-center text-white pt-4 text-2xl top-0 rounded-full block bg-neutral-600">+1</span>
</Tooltip.TooltipTrigger>
{truncatedAvatars.length !== 0 && (
<Tooltip.Content className="p-2 rounded-sm text-sm bg-black text-white shadow-sm">
<Tooltip.Content className="p-2 rounded-sm text-sm bg-brand text-white shadow-sm">
<Tooltip.Arrow />
<ul>
{truncatedAvatars.map((title) => (

View File

@@ -1,6 +1,7 @@
import classNames from "@lib/classNames";
import React from "react";
import classNames from "@lib/classNames";
export type BadgeProps = {
variant: "default" | "success" | "gray";
} & JSX.IntrinsicElements["span"];
@@ -12,7 +13,7 @@ export const Badge = function Badge(props: BadgeProps) {
<span
{...passThroughProps}
className={classNames(
"font-bold px-2 py-0.5 inline-block rounded-sm",
"font-bold px-2 py-0.5 inline-block rounded-sm text-xs",
variant === "default" && "bg-yellow-100 text-yellow-800",
variant === "success" && "bg-green-100 text-green-800",
variant === "gray" && "bg-gray-200 text-gray-800",

View File

@@ -1,22 +1,24 @@
import classNames from "@lib/classNames";
import Link, { LinkProps } from "next/link";
import React, { forwardRef } from "react";
type SVGComponent = React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
import classNames from "@lib/classNames";
import { SVGComponent } from "@lib/types/SVGComponent";
export type ButtonProps = {
export type ButtonBaseProps = {
color?: "primary" | "secondary" | "minimal" | "warn";
size?: "base" | "sm" | "lg" | "fab";
size?: "base" | "sm" | "lg" | "fab" | "icon";
loading?: boolean;
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
StartIcon?: SVGComponent;
EndIcon?: SVGComponent;
shallow?: boolean;
} & (
| (Omit<JSX.IntrinsicElements["a"], "href"> & { href: LinkProps["href"] })
| (JSX.IntrinsicElements["button"] & { href?: never })
);
};
export type ButtonProps = ButtonBaseProps &
(
| (Omit<JSX.IntrinsicElements["a"], "href"> & { href: LinkProps["href"] })
| (JSX.IntrinsicElements["button"] & { href?: never })
);
export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonProps>(function Button(
props: ButtonProps,
@@ -52,6 +54,8 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
size === "sm" && "px-3 py-2 text-sm leading-4 font-medium rounded-sm",
size === "base" && "px-3 py-2 text-sm font-medium rounded-sm",
size === "lg" && "px-4 py-2 text-base font-medium rounded-sm",
size === "icon" &&
"group p-2 border rounded-sm border-transparent text-neutral-400 hover:border-gray-200 transition",
// turn button into a floating action button (fab)
size === "fab" ? "fixed" : "relative",
size === "fab" && "justify-center bottom-20 right-8 rounded-full p-4 w-14 h-14",
@@ -60,7 +64,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
color === "primary" &&
(disabled
? "border border-transparent bg-gray-400 text-white"
: "border border-transparent text-white bg-neutral-900 hover:bg-neutral-800 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-neutral-900"),
: "border border-transparent dark:text-black text-white bg-brand dark:bg-white hover:bg-opacity-90 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-1 focus:ring-neutral-900"),
color === "secondary" &&
(disabled
? "border border-gray-200 text-gray-400 bg-white"
@@ -72,7 +76,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
color === "warn" &&
(disabled
? "text-gray-400 bg-transparent"
: "text-red-700 bg-transparent hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-red-50 focus:ring-red-500"),
: "text-gray-700 bg-transparent hover:text-red-700 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-red-50 focus:ring-red-500"),
// set not-allowed cursor if disabled
loading ? "cursor-wait" : disabled ? "cursor-not-allowed" : "",
props.className
@@ -85,14 +89,21 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
: props.onClick,
},
<>
{StartIcon && <StartIcon className="inline w-5 h-5 mr-2 -ml-1" />}
{StartIcon && (
<StartIcon
className={classNames(
"inline",
size === "icon" ? "w-5 h-5 group-hover:text-black" : "w-5 h-5 mr-2 -ml-1"
)}
/>
)}
{props.children}
{loading && (
<div className="absolute transform -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2">
<svg
className={classNames(
"w-5 h-5 mx-4 animate-spin",
color === "primary" ? "text-white" : "text-black"
color === "primary" ? "dark:text-black text-white" : "text-black"
)}
xmlns="http://www.w3.org/2000/svg"
fill="none"

View File

@@ -0,0 +1,28 @@
import Link from "next/link";
import { useLocale } from "@lib/hooks/useLocale";
const PoweredByCal = () => {
const { t } = useLocale();
return (
<div className="text-xs text-center sm:text-right p-1">
<Link href={`https://cal.com?utm_source=embed&utm_medium=powered-by-button`}>
<a target="_blank" className="dark:text-white text-gray-500 opacity-50 hover:opacity-100">
{t("powered_by")}{" "}
<img
className="dark:hidden w-auto inline h-[10px] relative -mt-px"
src="https://cal.com/logo.svg"
alt="Cal.com Logo"
/>
<img
className="hidden dark:inline w-auto h-[10px] relativ -mt-px"
src="https://cal.com/logo-white.svg"
alt="Cal.com Logo"
/>
</a>
</Link>
</div>
);
};
export default PoweredByCal;

View File

@@ -1,25 +0,0 @@
import Link from "next/link";
const PoweredByCalendso = () => (
<div className="text-xs text-center sm:text-right p-1">
<Link href={`https://cal.com?utm_source=embed&utm_medium=powered-by-button`}>
<a target="_blank" className="dark:text-white text-gray-500 opacity-50 hover:opacity-100">
powered by{" "}
<img
style={{ top: -2 }}
className="dark:hidden w-auto inline h-3 relative"
src="/calendso-logo-word.svg"
alt="Cal.com Logo"
/>
<img
style={{ top: -2 }}
className="hidden dark:inline w-auto h-3 relative"
src="/calendso-logo-word-dark.svg"
alt="Cal.com Logo"
/>
</a>
</Link>
</div>
);
export default PoweredByCalendso;

View File

@@ -1,333 +0,0 @@
import React from "react";
import Text from "@components/ui/Text";
import { PlusIcon, TrashIcon } from "@heroicons/react/outline";
import dayjs, { Dayjs } from "dayjs";
import classnames from "classnames";
export const SCHEDULE_FORM_ID = "SCHEDULE_FORM_ID";
export const toCalendsoAvailabilityFormat = (schedule: Schedule) => {
return schedule;
};
export const _24_HOUR_TIME_FORMAT = `HH:mm:ss`;
const DEFAULT_START_TIME = "09:00:00";
const DEFAULT_END_TIME = "17:00:00";
/** Begin Time Increments For Select */
const increment = 15;
/**
* Creates an array of times on a 15 minute interval from
* 00:00:00 (Start of day) to
* 23:45:00 (End of day with enough time for 15 min booking)
*/
const TIMES = (() => {
const starting_time = dayjs().startOf("day");
const ending_time = dayjs().endOf("day");
const times = [];
let t: Dayjs = starting_time;
while (t.isBefore(ending_time)) {
times.push(t);
t = t.add(increment, "minutes");
}
return times;
})();
/** End Time Increments For Select */
const DEFAULT_SCHEDULE: Schedule = {
monday: [{ start: "09:00:00", end: "17:00:00" }],
tuesday: [{ start: "09:00:00", end: "17:00:00" }],
wednesday: [{ start: "09:00:00", end: "17:00:00" }],
thursday: [{ start: "09:00:00", end: "17:00:00" }],
friday: [{ start: "09:00:00", end: "17:00:00" }],
saturday: null,
sunday: null,
};
type DayOfWeek = "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday" | "sunday";
export type TimeRange = {
start: string;
end: string;
};
export type FreeBusyTime = TimeRange[];
export type Schedule = {
monday?: FreeBusyTime | null;
tuesday?: FreeBusyTime | null;
wednesday?: FreeBusyTime | null;
thursday?: FreeBusyTime | null;
friday?: FreeBusyTime | null;
saturday?: FreeBusyTime | null;
sunday?: FreeBusyTime | null;
};
type ScheduleBlockProps = {
day: DayOfWeek;
ranges?: FreeBusyTime | null;
selected?: boolean;
};
type Props = {
schedule?: Schedule;
onChange?: (data: Schedule) => void;
onSubmit: (data: Schedule) => void;
};
const SchedulerForm = ({ schedule = DEFAULT_SCHEDULE, onSubmit }: Props) => {
const ref = React.useRef<HTMLFormElement>(null);
const transformElementsToSchedule = (elements: HTMLFormControlsCollection): Schedule => {
const schedule: Schedule = {};
const formElements = Array.from(elements)
.map((element) => {
return element.id;
})
.filter((value) => value);
/**
* elementId either {day} or {day.N.start} or {day.N.end}
* If elementId in DAYS_ARRAY add elementId to scheduleObj
* then element is the checkbox and can be ignored
*
* If elementId starts with a day in DAYS_ARRAY
* the elementId should be split by "." resulting in array length 3
* [day, rangeIndex, "start" | "end"]
*/
formElements.forEach((elementId) => {
const [day, rangeIndex, rangeId] = elementId.split(".");
if (rangeIndex && rangeId) {
if (!schedule[day]) {
schedule[day] = [];
}
if (!schedule[day][parseInt(rangeIndex)]) {
schedule[day][parseInt(rangeIndex)] = {};
}
schedule[day][parseInt(rangeIndex)][rangeId] = elements[elementId].value;
}
});
return schedule;
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const elements = ref.current?.elements;
if (elements) {
const schedule = transformElementsToSchedule(elements);
onSubmit && typeof onSubmit === "function" && onSubmit(schedule);
}
};
const ScheduleBlock = ({ day, ranges: defaultRanges, selected: defaultSelected }: ScheduleBlockProps) => {
const [ranges, setRanges] = React.useState(defaultRanges);
const [selected, setSelected] = React.useState(defaultSelected);
React.useEffect(() => {
if (!ranges || ranges.length === 0) {
setSelected(false);
} else {
setSelected(true);
}
}, [ranges]);
const handleSelectedChange = () => {
if (!selected && (!ranges || ranges.length === 0)) {
setRanges([
{
start: "09:00:00",
end: "17:00:00",
},
]);
}
setSelected(!selected);
};
const handleAddRange = () => {
let rangeToAdd;
if (!ranges || ranges?.length === 0) {
rangeToAdd = {
start: DEFAULT_START_TIME,
end: DEFAULT_END_TIME,
};
setRanges([rangeToAdd]);
} else {
const lastRange = ranges[ranges.length - 1];
const [hour, minute, second] = lastRange.end.split(":");
const date = dayjs()
.set("hour", parseInt(hour))
.set("minute", parseInt(minute))
.set("second", parseInt(second));
const nextStartTime = date.add(1, "hour");
const nextEndTime = date.add(2, "hour");
/**
* If next range goes over into "tomorrow"
* i.e. time greater that last value in Times
* return
*/
if (nextStartTime.isAfter(date.endOf("day"))) {
return;
}
rangeToAdd = {
start: nextStartTime.format(_24_HOUR_TIME_FORMAT),
end: nextEndTime.format(_24_HOUR_TIME_FORMAT),
};
setRanges([...ranges, rangeToAdd]);
}
};
const handleDeleteRange = (range: TimeRange) => {
if (ranges && ranges.length > 0) {
setRanges(
ranges.filter((r: TimeRange) => {
return r.start != range.start;
})
);
}
};
/**
* Should update ranges values
*/
const handleSelectRangeChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const [day, rangeIndex, rangeId] = event.currentTarget.name.split(".");
if (day && ranges) {
const newRanges = ranges.map((range, index) => {
const newRange = {
...range,
[rangeId]: event.currentTarget.value,
};
return index === parseInt(rangeIndex) ? newRange : range;
});
setRanges(newRanges);
}
};
const TimeRangeField = ({ range, day, index }: { range: TimeRange; day: DayOfWeek; index: number }) => {
const timeOptions = (type: "start" | "end") =>
TIMES.map((time) => (
<option
key={`${day}.${index}.${type}.${time.format(_24_HOUR_TIME_FORMAT)}`}
value={time.format(_24_HOUR_TIME_FORMAT)}>
{time.toDate().toLocaleTimeString(undefined, { minute: "numeric", hour: "numeric" })}
</option>
));
return (
<div key={`${day}-range-${index}`} className="flex items-center justify-between space-x-2">
<div className="flex items-center space-x-2">
<select
id={`${day}.${index}.start`}
name={`${day}.${index}.start`}
defaultValue={range?.start || DEFAULT_START_TIME}
onChange={handleSelectRangeChange}
className="block px-4 pr-8 py-2 text-base border-gray-300 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm rounded-sm">
{timeOptions("start")}
</select>
<Text>-</Text>
<select
id={`${day}.${index}.end`}
name={`${day}.${index}.end`}
defaultValue={range?.end || DEFAULT_END_TIME}
onChange={handleSelectRangeChange}
className=" block px-4 pr-8 py-2 text-base border-gray-300 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm rounded-sm">
{timeOptions("end")}
</select>
</div>
<div className="">
<DeleteAction range={range} />
</div>
</div>
);
};
const Actions = () => {
return (
<div className="flex items-center space-x-2">
<button type="button" onClick={() => handleAddRange()}>
<PlusIcon className="h-5 w-5 text-neutral-400 hover:text-neutral-500" />
</button>
</div>
);
};
const DeleteAction = ({ range }: { range: TimeRange }) => {
return (
<button type="button" onClick={() => handleDeleteRange(range)}>
<TrashIcon className="h-5 w-5 text-neutral-400 hover:text-neutral-500" />
</button>
);
};
return (
<fieldset className=" py-6">
<section
className={classnames(
"flex flex-col space-y-6 sm:space-y-0 sm:flex-row sm:justify-between",
ranges && ranges?.length > 1 ? "sm:items-start" : "sm:items-center"
)}>
<div style={{ minWidth: "33%" }} className="flex items-center justify-between">
<div className="flex items-center space-x-2 ">
<input
id={day}
name={day}
checked={selected}
onChange={handleSelectedChange}
type="checkbox"
className="focus:ring-neutral-500 h-4 w-4 text-neutral-900 border-gray-300 rounded-sm"
/>
<Text variant="overline">{day}</Text>
</div>
<div className="sm:hidden justify-self-end self-end">
<Actions />
</div>
</div>
<div className="space-y-2 w-full">
{selected && ranges && ranges.length != 0 ? (
ranges.map((range, index) => (
<TimeRangeField key={`${day}-range-${index}`} range={range} index={index} day={day} />
))
) : (
<Text key={`${day}`} variant="caption">
Unavailable
</Text>
)}
</div>
<div className="hidden sm:block px-2">
<Actions />
</div>
</section>
</fieldset>
);
};
return (
<>
<form id={SCHEDULE_FORM_ID} onSubmit={handleSubmit} ref={ref} className="divide-y divide-gray-200">
{Object.keys(schedule).map((day) => {
const selected = schedule[day as DayOfWeek] != null;
return (
<ScheduleBlock
key={`${day}`}
day={day as DayOfWeek}
ranges={schedule[day as DayOfWeek]}
selected={selected}
/>
);
})}
</form>
</>
);
};
export default SchedulerForm;

View File

@@ -1,12 +1,16 @@
import React, { useEffect, useState } from "react";
import TimezoneSelect from "react-timezone-select";
import { TrashIcon } from "@heroicons/react/outline";
import { Availability } from "@prisma/client";
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import React, { useEffect, useState } from "react";
import TimezoneSelect, { ITimezoneOption } from "react-timezone-select";
import { useLocale } from "@lib/hooks/useLocale";
import { OpeningHours, DateOverride } from "@lib/types/event-type";
import { WeekdaySelect } from "./WeekdaySelect";
import SetTimesModal from "./modal/SetTimesModal";
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import { Availability } from "@prisma/client";
dayjs.extend(utc);
dayjs.extend(timezone);
@@ -14,43 +18,30 @@ dayjs.extend(timezone);
type Props = {
timeZone: string;
availability: Availability[];
setTimeZone: unknown;
setTimeZone: (timeZone: string) => void;
setAvailability: (schedule: { openingHours: OpeningHours[]; dateOverrides: DateOverride[] }) => void;
};
export const Scheduler = ({
availability,
setAvailability,
timeZone: selectedTimeZone,
setTimeZone,
}: Props) => {
/**
* @deprecated
*/
export const Scheduler = ({ availability, setAvailability, timeZone, setTimeZone }: Props) => {
const { t, i18n } = useLocale();
const [editSchedule, setEditSchedule] = useState(-1);
const [dateOverrides, setDateOverrides] = useState([]);
const [openingHours, setOpeningHours] = useState([]);
const [openingHours, setOpeningHours] = useState<Availability[]>([]);
useEffect(() => {
setOpeningHours(
availability
.filter((item: Availability) => item.days.length !== 0)
.map((item) => {
item.startDate = dayjs().utc().startOf("day").add(item.startTime, "minutes");
item.endDate = dayjs().utc().startOf("day").add(item.endTime, "minutes");
return item;
})
);
setDateOverrides(availability.filter((item: Availability) => item.date));
setOpeningHours(availability.filter((item: Availability) => item.days.length !== 0));
}, []);
// updates availability to how it should be formatted outside this component.
useEffect(() => {
setAvailability({
dateOverrides: dateOverrides,
openingHours: openingHours,
});
}, [dateOverrides, openingHours]);
setAvailability({ openingHours, dateOverrides: [] });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [openingHours]);
const addNewSchedule = () => setEditSchedule(openingHours.length);
const applyEditSchedule = (changed) => {
const applyEditSchedule = (changed: Availability) => {
// new entry
if (!changed.days) {
changed.days = [1, 2, 3, 4, 5]; // Mon - Fri
@@ -59,39 +50,33 @@ export const Scheduler = ({
// update
const replaceWith = { ...openingHours[editSchedule], ...changed };
openingHours.splice(editSchedule, 1, replaceWith);
setOpeningHours([].concat(openingHours));
setOpeningHours([...openingHours]);
}
};
const removeScheduleAt = (toRemove: number) => {
openingHours.splice(toRemove, 1);
setOpeningHours([].concat(openingHours));
setOpeningHours([...openingHours]);
};
const OpeningHours = ({ idx, item }) => (
<li className="py-2 flex justify-between border-b">
const OpeningHours = ({ idx, item }: { idx: number; item: Availability }) => (
<li className="flex justify-between py-2 border-b">
<div className="flex flex-col space-y-4 lg:inline-flex">
<WeekdaySelect defaultValue={item.days} onSelect={(selected: number[]) => (item.days = selected)} />
<button
className="text-sm bg-neutral-100 rounded-sm py-2 px-3"
className="px-3 py-2 text-sm rounded-sm bg-neutral-100"
type="button"
onClick={() => setEditSchedule(idx)}>
{dayjs()
.startOf("day")
.add(item.startTime, "minutes")
.format(item.startTime % 60 === 0 ? "ha" : "h:mma")}
&nbsp;until&nbsp;
{dayjs()
.startOf("day")
.add(item.endTime, "minutes")
.format(item.endTime % 60 === 0 ? "ha" : "h:mma")}
{item.startTime.toLocaleTimeString(i18n.language, { hour: "numeric", minute: "2-digit" })}
&nbsp;{t("until")}&nbsp;
{item.endTime.toLocaleTimeString(i18n.language, { hour: "numeric", minute: "2-digit" })}
</button>
</div>
<button
type="button"
onClick={() => removeScheduleAt(idx)}
className="btn-sm bg-transparent px-2 py-1 ml-1">
<TrashIcon className="h-5 w-5 inline text-gray-400 -mt-1" />
className="px-2 py-1 ml-1 bg-transparent btn-sm">
<TrashIcon className="inline w-5 h-5 -mt-1 text-gray-400" />
</button>
</li>
);
@@ -100,16 +85,16 @@ export const Scheduler = ({
<div>
<div className="flex">
<div className="w-full">
<div className="">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
Timezone
{t("timezone")}
</label>
<div className="mt-1">
<TimezoneSelect
id="timeZone"
value={{ value: selectedTimeZone }}
onChange={(tz) => setTimeZone(tz.value)}
className="shadow-sm focus:ring-black focus:border-black mt-1 block w-full sm:text-sm border-gray-300 rounded-md"
value={timeZone}
onChange={(tz: ITimezoneOption) => setTimeZone(tz.value)}
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-black focus:border-brand sm:text-sm"
/>
</div>
</div>
@@ -118,16 +103,36 @@ export const Scheduler = ({
<OpeningHours key={idx} idx={idx} item={item} />
))}
</ul>
<button type="button" onClick={addNewSchedule} className="btn-white btn-sm mt-2">
Add another
<button type="button" onClick={addNewSchedule} className="mt-2 btn-white btn-sm">
{t("add_another")}
</button>
</div>
</div>
{editSchedule >= 0 && (
<SetTimesModal
startTime={openingHours[editSchedule] ? openingHours[editSchedule].startTime : 540}
endTime={openingHours[editSchedule] ? openingHours[editSchedule].endTime : 1020}
onChange={(times) => applyEditSchedule({ ...(openingHours[editSchedule] || {}), ...times })}
startTime={
openingHours[editSchedule]
? new Date(openingHours[editSchedule].startTime).getHours() * 60 +
new Date(openingHours[editSchedule].startTime).getMinutes()
: 540
}
endTime={
openingHours[editSchedule]
? new Date(openingHours[editSchedule].endTime).getHours() * 60 +
new Date(openingHours[editSchedule].endTime).getMinutes()
: 1020
}
onChange={(times: { startTime: number; endTime: number }) =>
applyEditSchedule({
...(openingHours[editSchedule] || {}),
startTime: new Date(
new Date().setHours(Math.floor(times.startTime / 60), times.startTime % 60, 0, 0)
),
endTime: new Date(
new Date().setHours(Math.floor(times.endTime / 60), times.endTime % 60, 0, 0)
),
})
}
onExit={() => setEditSchedule(-1)}
/>
)}

View File

@@ -1,12 +1,14 @@
import { useState } from "react";
import * as PrimitiveSwitch from "@radix-ui/react-switch";
import { useId } from "@radix-ui/react-id";
import * as Label from "@radix-ui/react-label";
import * as PrimitiveSwitch from "@radix-ui/react-switch";
import React, { useState } from "react";
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
import classNames from "@lib/classNames";
export default function Switch(props) {
type SwitchProps = React.ComponentProps<typeof PrimitiveSwitch.Root> & {
label: string;
};
export default function Switch(props: SwitchProps) {
const { label, onCheckedChange, ...primitiveProps } = props;
const [checked, setChecked] = useState(props.defaultChecked || false);
@@ -16,7 +18,7 @@ export default function Switch(props) {
}
setChecked(change);
};
const id = useId();
return (
<div className="flex items-center h-[20px]">
<PrimitiveSwitch.Root
@@ -25,6 +27,7 @@ export default function Switch(props) {
onCheckedChange={onPrimitiveCheckedChange}
{...primitiveProps}>
<PrimitiveSwitch.Thumb
id={id}
className={classNames(
"bg-white w-[16px] h-[16px] block transition-transform",
checked ? "translate-x-[16px]" : "translate-x-0"
@@ -32,7 +35,9 @@ export default function Switch(props) {
/>
</PrimitiveSwitch.Root>
{label && (
<Label.Root className="text-neutral-700 align-text-top ml-3 font-medium cursor-pointer">
<Label.Root
htmlFor={id}
className="text-neutral-700 text-sm align-text-top ml-3 font-medium cursor-pointer">
{label}
</Label.Root>
)}

View File

@@ -0,0 +1,96 @@
import { Menu, Transition } from "@headlessui/react";
import { DotsHorizontalIcon } from "@heroicons/react/solid";
import React, { FC, Fragment } from "react";
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
import { SVGComponent } from "@lib/types/SVGComponent";
import Button from "./Button";
export type ActionType = {
id: string;
icon: SVGComponent;
label: string;
disabled?: boolean;
color?: "primary" | "secondary";
} & ({ href?: never; onClick: () => any } | { href: string; onClick?: never });
interface Props {
actions: ActionType[];
}
const TableActions: FC<Props> = ({ actions }) => {
const { t } = useLocale();
return (
<>
<div className="space-x-2 hidden lg:block">
{actions.map((action) => (
<Button
key={action.id}
data-testid={action.id}
href={action.href}
onClick={action.onClick}
StartIcon={action.icon}
disabled={action.disabled}
color={action.color || "secondary"}>
{action.label}
</Button>
))}
</div>
<Menu as="div" className="inline-block lg:hidden text-left ">
{({ open }) => (
<>
<div>
<Menu.Button className="text-neutral-400 mt-1 p-2 border border-transparent hover:border-gray-200">
<span className="sr-only">{t("open_options")}</span>
<DotsHorizontalIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
show={open}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95">
<Menu.Items
static
className="origin-top-right absolute right-0 mt-2 w-56 rounded-sm shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none divide-y divide-neutral-100">
<div className="py-1">
{actions.map((action) => {
const Element = typeof action.onClick === "function" ? "span" : "a";
return (
<Menu.Item key={action.id} disabled={action.disabled}>
{({ active }) => (
<Element
href={action.href}
onClick={action.onClick}
className={classNames(
active ? "bg-neutral-100 text-neutral-900" : "text-neutral-700",
"group flex items-center px-4 py-2 text-sm font-medium"
)}>
<action.icon
className="mr-3 h-5 w-5 text-neutral-400 group-hover:text-neutral-500"
aria-hidden="true"
/>
{action.label}
</Element>
)}
</Menu.Item>
);
})}
</div>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</>
);
};
export default TableActions;

View File

@@ -1,9 +1,10 @@
import React from "react";
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Body: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-lg leading-relaxed text-gray-900 dark:text-white", props?.className);
const classes = classnames("text-gray-900 dark:text-white", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};

View File

@@ -1,2 +1,3 @@
import Body from "./Body";
export default Body;

View File

@@ -1,5 +1,6 @@
import React from "react";
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Caption: React.FunctionComponent<TextProps> = (props: TextProps) => {

View File

@@ -1,2 +1,3 @@
import Caption from "./Caption";
export default Caption;

View File

@@ -1,5 +1,6 @@
import React from "react";
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Caption2: React.FunctionComponent<TextProps> = (props: TextProps) => {

View File

@@ -1,2 +1,3 @@
import Caption2 from "./Caption2";
export default Caption2;

View File

@@ -1,5 +1,6 @@
import React from "react";
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Footnote: React.FunctionComponent<TextProps> = (props: TextProps) => {

View File

@@ -1,2 +1,3 @@
import Footnote from "./Footnote";
export default Footnote;

View File

@@ -1,9 +1,10 @@
import React from "react";
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Headline: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-xl font-bold text-gray-900 dark:text-white", props?.className);
const classes = classnames("font-cal text-xl font-bold text-gray-900 dark:text-white", props?.className);
return <p className={classes}>{props?.text || props.children}</p>;
};

View File

@@ -1,2 +1,3 @@
import Headline from "./Headline";
export default Headline;

View File

@@ -1,9 +1,13 @@
import React from "react";
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Largetitle: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames("text-3xl font-extrabold text-gray-900 dark:text-white", props?.className);
const classes = classnames(
"font-cal tracking-wider text-3xl text-gray-900 dark:text-white mb-2",
props?.className
);
return <p className={classes}>{props?.text || props.children}</p>;
};

View File

@@ -1,2 +1,3 @@
import Largetitle from "./Largetitle";
export default Largetitle;

View File

@@ -1,10 +1,11 @@
import React from "react";
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Overline: React.FunctionComponent<TextProps> = (props: TextProps) => {
const classes = classnames(
"text-sm uppercase font-semibold leading-snug tracking-wide text-gray-900 dark:text-white",
"text-sm capitalize font-medium text-gray-900 dark:text-white",
props?.className
);

View File

@@ -1,2 +1,3 @@
import Overline from "./Overline";
export default Overline;

View File

@@ -1,5 +1,6 @@
import React from "react";
import classnames from "classnames";
import React from "react";
import { TextProps } from "../Text";
const Subheadline: React.FunctionComponent<TextProps> = (props: TextProps) => {

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