+25









Morgan
GitHub
Erik
Morgan Vernay
Lauris Skraucis
Peer Richelsen
Udit Takkar
Dmytro Hryshyn
DmytroHryshyn
zomars
Udit Takkar
sean-brydon
Crowdin Bot
Pratik Kumar
Samyabrata Maji
Manpreet Singh
Benny Joo
Varun Prahlad Balani
Mike Zhou
Haran Rajkumar
Harshith Pabbati
Brendan Woodward
Ryukemeister
Keith Williams
Alex van Andel
Joe Au-Yeung
gitstart-calcom
GitStart-Cal.com
gitstart-calcom
Lauris
Amit Sharma
Hariom Balhara
gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Anant Jain
supalarry
0a15483c46
* add connect to cal atom * add connect to cal to atoms package index * shadcn setup * update packages * button component from shadcn * dumb button component * replace button with dumb button component * update commenst and change url param * chore: setting up monorepo packages for platforms (#12572) * chore: init platform folders strucutre and api proxy * fixup! chore: init platform folders strucutre and api proxy * chore: init database oAuth platform client schema * fixup! chore: init database oAuth platform client schema * fix config path after move to folder /v1 * feat: scaffold v2 API (#12554) * init nest project * add kysely as a separate package * add ee folder with license * prettier: format v2 api same as mono repo * migrate over Eriks nestjs poc * schema generation script * ee license file * nest poc eslint file * remove prisma-kysely from v2 api * prepare for mergin platform: remove prisma-kysely because incorrect checksum * add prisma-kysely to packages/prisma * move to apps/api/v2 * rename v2 package * readme update * Update apps/api/v2/env.example * env typo * env typo --------- Co-authored-by: Erik <erik@erosemberg.com> Co-authored-by: Morgan Vernay <morgan@cal.com> * chore: init platform packages (#12603) * chore: init platform packages * fixup! chore: init platform packages * fixup! fixup! chore: init platform packages * fix: type in package.json * fixup! fixup! fixup! chore: init platform packages * fixup! fixup! fixup! fixup! chore: init platform packages * fixup! fixup! fixup! fixup! fixup! chore: init platform packages * fixup! fixup! fixup! fixup! fixup! fixup! chore: init platform packages --------- Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com> * chore: platform more permissions utils and tests (#12613) * chore: more permisions utils and tests * chore: tests * feat: validate `CalProvider` api keys (#12672) * fix: signup nit (#12585) * Disable submit on empty form * Fix submit --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com> * New Crowdin translations by Github Action * fix: Signup options are not disabled (#12610) * fix: Signup options are not disabled * fixes/signup disabling suggested changes done * chore: signup and login improvements --------- Co-authored-by: Udit Takkar <udit222001@gmail.com> * fix: typo in @calcom/emails readme (#12615) * fix: handle reschedule request for dynamic meetings (#12275) * chore: [app-router-migration-1] migrate the pages in `settings/admin` to the app directory (#12561) Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com> Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> * chore: added cursor-pointer to img upload (#12624) * added cursor-pointer to img upload * nit * feat: add clear filters option in bookings page (#12629) * add clear filters option * fix vscode settings.json * use removeAllQueryParams() * fix yarn lock * remove toggleoption * feat: display long durations in hours on booking (#12631) Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> * New Crowdin translations by Github Action * fix: workaround for future app dir routes * feat: Allow only first slot to be booked (#12636) Co-authored-by: Morgan Vernay <morgan@cal.com> * New Crowdin translations by Github Action * feat: add matomo analytics app (#12646) * chore: Sentry Wrapper with Performance and Error Tracing (#12642) * add wrapper for sentry and update functions in 'getUserAvailability'. Update tracesSampleRate to 1.0 * Make Sentry Wrapper utilize parent transaction, if it exists. * Update wrapper for functions to inherit parameters from the child function * add comment of when to use the wrapper * check for sentry before wrapping, if not call unwrapped function * refactored wrapper to have async and sync separate functions that utilize helpers for common behaviour * update type of args to unknown * fixed types of returns from wrapped functions --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> * validate api keys to set error and key states for api keys * organize error messages into one place * set error messages from errors file instead of hardcoding value * fix incorrect constant name --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: Pratik Kumar <70286186+Pratik-Kumar-621@users.noreply.github.com> Co-authored-by: Udit Takkar <udit222001@gmail.com> Co-authored-by: Samyabrata Maji <116789799+samyabrata-maji@users.noreply.github.com> Co-authored-by: Manpreet Singh <manpoffc@gmail.com> Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com> Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Varun Prahlad Balani <varunprahladbalani@gmail.com> Co-authored-by: Mike Zhou <mikezhoudev@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Haran Rajkumar <haranrajkumar97@gmail.com> Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: Harshith Pabbati <pabbatiharshith@gmail.com> Co-authored-by: Brendan Woodward <73412688+bwoody13@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> * fix: storybook atom import * feat: oauth clients management (#12604) * feat: oAuth create and update dtos * feat: WIP oAuth repository * feat: oAuth controller and module * refactor: rename cal oAuth to simply oauth * fix: controller update client * add oAuth client module to endpoints module * refactor: OAuthClientRepository * refactor * status codes for controller * oauth create return * fix: oAuth client only for v2 API * remove repository as provider from its module * fix: oAuth module, have repository in providers * fix: make endpoints run * comment out oauth prisma schema code: coming next * refactor: remove api-key authguard (#12675) * chore: Prisma exception filters (#12679) * fix: get prisma schema from package @calcom/prisma (#12692) * fix: get prisma schema from package @calcom/prisma * fixup! fix: get prisma schema from package @calcom/prisma * fixup! fixup! fix: get prisma schema from package @calcom/prisma * fix: prettier config * fixup! fix: prettier config * chore: create authenticated user guard (#12677) * chore: wip next-auth-guard for apiv2 * refactor: use NextAuthGuard on all OAuthClientController routes * refactor: use passport to get next user * exclude password from prisma read service * Revert "exclude password from prisma read service" This reverts commit 24b1f8ed59f6d99f0ecc589c691efb0ac1f2ac3b. * fix: userRepo sanitize password * refactor: function order in UserRepository * implement user repository service * refactor: strict null checks for TS * refactor: fix strict null checks * delete res locals type * refactor: switch from zod to class-validator for oAuth client * refactor: switch from zod to class-validator for booking * refactor: rename dtos folder to input folder * fix: create migration for prisma schema oauth clients table * feature: post oAuth client * refactor: rename oAuth client_secret to secret * stricter ts config + oauth secret * remove migrations * fix: improve response type * improve error code types * fix: prettier by removing eslint as its not needed * refactor: eslint fix files * wip organization oauth_lcient * fix import types * fix team schema platformOAuthClient name * fix: import packages transpiled * feat: roles guard * refactor: use organisationId instead of userId * fix: jwt secret signing not having jwt_secret * fix: stop importing input DTOs as types * fix: jwt sign expecting plain object * add sentry, routes logger, generic response type * fix test config * fixup! fix test config * refactor: dont await async function return * feat: fallback value for getEnv * feat: add helmet for security * feat: organization guard * refactor: check if user set in RolesGuard * refactor: remove roles requirement for oauth read * chore: add exceptions filters * fix: api key strategy throws errors * chore: set e2e tests * chore: centralize api type in package * feat: user repository relations * feat: user repository relations * Revert "feat: user repository relations" This reverts commit 1802b256b5a6e5c9b6a62138c8cb1b6f75ed401b. * Revert "feat: user repository relations" This reverts commit 4a8f52583d95ff15b6254694c976d8ec9ce0e546. * test: mock next auth * fixup! test: mock next auth * test:oAuth authed user but no org * refactor: remove unused type * refactor: organisation guard based on Membership table * refactor: folder and file structure * fix: small changes exception filters * refactor: merge organization and roles guards into one guard * fix: remove old export * fix: org roles guard --------- Co-authored-by: supalarry <lauris.skraucis@gmail.com> * chore: OAuth clients tests and fixes (#12762) * chore: add tests and fixes * fixup! chore: add tests and fixes * fixup! fixup! chore: add tests and fixes * fixup! fixup! fixup! chore: add tests and fixes * fixup! fixup! fixup! fixup! chore: add tests and fixes * fixup! fixup! fixup! fixup! fixup! chore: add tests and fixes * fixup! fixup! fixup! fixup! fixup! fixup! chore: add tests and fixes * fix: remove unused imports * chore: move repos to feature folder (#12769) * chore: rename repositories * fix yarn.lock * feat: oauth client guard (#12814) * feat: oAuth client guard * refactor test * refactor: move oauth-client guard to oauth module * refactor: separate jest config from package.json * fix: resolving paths in jest tests * fix: tests * jest setup file * fix: jest test warnings about .js platform constants imports * refactor: test repository fixtures * remove allowjs * ignore js files in ts-jest * make oauth client module global * make oauth client module global * feat: platform oAuth clients frontend (#12867) * add oauth client to settings * fix imports * add react and axios * oauth client form and card components * hooks for oauth clients data * index page for oauth clients * oauth client list component * oauth client form page * shift atoms into platform * init platform folder * refactor handleSubmit functioin * platform * platform * platform parts * revert tsconfig constant platform * fix: useOauthClients * feat: create oauth client with api * fix: add prettier to platform type package * fixup! fix: add prettier to platform type package * chore: class-validator types in platform package * add types for delete oauth client iput * add onSuccess and onError methods * update oauth client card view with client id and secret * cleanup comments * split oauth persisit hook into create and delete hooks * fix: oauth client creation / deletion / listing * fixup! fix: oauth client creation / deletion / listing * fix: comment logo for now * fix: layout setting org keys * cleanup comments * minor style fixes, add logic for client permissions * show toast after deleting client * not passing clint logo at the moment --------- Co-authored-by: Morgan Vernay <morgan@cal.com> * chore(platform): OAuth Flow (#12798) * fixup! chore(platform): OAuth Flow (#12798) * feat: CRUD for oAuth users (#12853) * user controller with oauth guard * crud repository and controller operations * add user controller to user module * refactor: oauth guard * connect user with oauth client upon creation * fix: test * wip: e2e test * refactor: findUniqueOrThrow -> findUnique user * test: POST user request * feat: permissions guard * e2e tests * e2e * e2e refactor * e2e refactor * reflector decorator check refactor * refactor oauth guard * remov unused imports * log message * delete permissions decorator and guard * remove delete user endpoint * remove delete user endpoint * refactor: route structure * remove get oauth client decorator * delete unecessary e2e config changes * remove set header in post test * fix: oauth guard test on empty db * revert: add previously removed constant to fulfill merge platform * fix: import to satisfy platform branch merge * use real implementation of access token guard * generate access & refresh tokens and fix e2e * fix: oauth client e2e test * refactor: variable naming * refactor * rename test file * remove oauth client from request * refactor: v2 API (#12913) * Use Boolean only instead of git add src/modules/auth/guard/organization-roles/organization-roles.guard.ts * move tests next to files they test * replace .. in import paths with absolute path * camelCase instead of snake_case for access and refresh token variables * user sanitize function Typescript friendly * restructure oAuth clients folder: example for other folders * restructure bookings module * organize modules in auth, endpoints, repositories, services * organize auth module * organize repositories * organize inputs * rename OAuthClientGuard to OAuthClientCredentialsGuard * add error messages * add error messages * clientId as param in oauth-flow & schema mapping * camelCase instead of snake_case for clientId and clientSecret * access token guard as passport strategy * folder structure as features * get rid of index files * feat: endpoint for deleting oAuth users & oAuth users returned data (#12912) * feat: delete oAuth users * check if access token matches userId in parameter * driveby: return only user id and email in oauth users endpoints * refactor: access token errors (#12932) * change error messages * error message if access token does not exist in our database * token expired 498 * access token expired message as constant * store 498 status code as const * fix: access token error less information given * chore: invalid access token const --------- Co-authored-by: Morgan Vernay <morgan@cal.com> * feat: external gcal (#12954) * wip connect gcal * feat: add external google calendar * fixup! feat: add external google calendar * fix: google calendar oauth check invalid * google cal check tests * use zod * fix: await validate access token service * chore: e2e tests for oAuth flow (#13005) * fix: accessing length of undefined * refactor: GetUser throw error if no user provided * fix: cascade delete PlatformAuthorizationToken if owner or client deleted * test: POST /authorize * refactor oauth-flow controller * refactor oauth-flow controller * new function to get authorization token by client user ids * refactor token service * fix: re-created access and refresh tokens having not unique secret * oauth flow tests * oauth flow tests * feat: abstracted jwt service (#13016) * remove unused JwtModule from the auth module * feat: create abstracted jwt service * refactor: tokens module and service use new jwt service * refactor: oauth-client module and repository use new jwt service * implement Morgans requests * Basic atoms in barebone example platform apps (#13006) * example app * example app * dev move * fix: more entry points * fixup! fix: more entry points * refactor: v2 API (#12913) * Use Boolean only instead of git add src/modules/auth/guard/organization-roles/organization-roles.guard.ts * move tests next to files they test * replace .. in import paths with absolute path * camelCase instead of snake_case for access and refresh token variables * user sanitize function Typescript friendly * restructure oAuth clients folder: example for other folders * restructure bookings module * organize modules in auth, endpoints, repositories, services * organize auth module * organize repositories * organize inputs * rename OAuthClientGuard to OAuthClientCredentialsGuard * add error messages * add error messages * clientId as param in oauth-flow & schema mapping * camelCase instead of snake_case for clientId and clientSecret * access token guard as passport strategy * folder structure as features * get rid of index files * feat: endpoint for deleting oAuth users & oAuth users returned data (#12912) * feat: delete oAuth users * check if access token matches userId in parameter * driveby: return only user id and email in oauth users endpoints * Connect CalProvider and GCal * Connect CalProvider and GCal * return response interceptor to handle failed requests * handle failed requests using axios intercepter * cal provider refresh tokens, external gcal * external gcal * cal provider refresh and retries * remove console.log * refactor * ignore built atoms css * remove change to token repo * refactor * refactor * downdgrade vite of unrelated packages * move gcal endpoints to platform * gcal service * refactor: use atoms provider --------- Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com> Co-authored-by: Ryukemeister <sahalrajiv-extc@atharvacoe.ac.in> * feat: user schedule management (#13053) * platform-constants package: list of accepted schedule timezones * feat: schedules endpoint to create schedule with default availability * refactor: rename function * refactor: store userId for availabilities * feat: createSchedule endpoint * feat: get schedules/default * feat: getSchedule by id * feat: get all schedules * feat: delete schedule * feat: update schedule * check user owns schedule * empty test * define returned data on controller level not repository * define returned data on controller level not repository * Revert "define returned data on controller level not repository" This reverts commit 4c292a0f0762f58bdcad0707ad8eef7408390800. * use luxton * put availabilities out of ee * use guard on controller level * refactor * e2e test schedule creation * remove log * test * default schedule get test * update schedule test * delete schedule test * fix update test * different email for schedules e2e * driveby: fix yarn test * schedule inputs availabilities as array * re-use BaseStrategy class * feat: me endpoint v2 api (#13274) * feat: me endpoint v2 api * simplify * chore: NestJS throttling (#13011) * fix: v2 throttling setup (#13402) * fix: v2 throttling * fix: setup redis locally * move sentry files to api v1 * chore: calcom platform libraries (#13444) * fixup! chore: calcom platform libraries (#13444) * fix: fix managed users * feat: user timezone management in `CalProvider` (#13361) * hooks to fetch and update user timezone * add react query package * add logic to check for change in user timezone and update it * hook to handle timezone changes * shift timezone handling logic into separate custom hook * rename variables * rename hook to get a user * call useTimezone hook inside of cal provider * minor refactors * add variables for api endpoints * use URL constructor for creating endpoints * key prop not needed * use URL constructor for creating endpoints * chore: package json scripts * fix(atoms): fix import path of useUpdateUserTimezone * fix(atoms): build constants and fix ENDPOINTS const * feat: v2 api event type endpoints (#13449) * build platform-libraries * make getEventTypeById compatible with default PrismaClient * feat: fetch event type endpoint * handle event type not found * e2e test for GET * test * determine is user org admin for getEventTypeId correctly * prisma type like before * add negative test * POST event-type and return event-type for api and atom * POST event-type and return event-type for api and atom * feat: create default user event types when creating managed user * feat: v2 api me PUT endpoint (#13486) * add timeZone to me endpoint * feat: me endpoint PUT * reset package.json * standardize creation and updating of user * user creation: capitalize timezones and week start * default event type constants in const file * refactor: defaultTimezone Europe/London in response schema * refactor: validators * refactor: default schedule in schedules endpoints * refactor: patch instead of put * fix: getEventTypeById organizationId (#13554) * chore: transpile more libraries functions * feat: overlay calendar busy dates events (#13581) * feat: overlay calendar busy times events * fixup! feat: overlay calendar busy times events * fixup! fixup! feat: overlay calendar busy times events * fixup! fixup! fixup! feat: overlay calendar busy times events * refactor: schedules for atom (#13585) * schedules get.handler transformers as util functions * export schedule transformers from platform-libraries package * schedules for atom * refactor: schedules response service format response * remove unused injected dependency * fix: prevent duplicate calendar account linking (#13310) * fix: prevent signing up multiple times from same account * revert: lark calendar changes * credential clean up if duplicate * fix code duplication and check before credential creation * feat: useRouterQuery allow unset queryParam * feat: showToast on account duplication attempt * Small tweak to copy * Updated other calendars not to use checkDuplicateCalendar * Add account already linked to apps/installed/calendar --------- Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> * Revert event-type profile based querying (#13588) * test: Bookings: Add more automated tests for organization (#13576) * Avoid selecting unused props * Add automated tests * Add existing user invite and booking --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> * fix: organization user shouldn't be asked for payment for a premium username (#13535) Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> * chore: refactor handle new reccuring booking (#13597) * chore: refactor handle new reccuring booking * fixup! chore: refactor handle new reccuring booking * fixup! fixup! chore: refactor handle new reccuring booking * test: Create unit tests for the questions (teste2e-multiSelectQuestion) (#11569) * Remove unnecessary changes * add changes * Requested changes * Requested changes * FIx failing tests * FIx failing tests * Update regularBookings.ts * FIx failing tests * Refactor * add unit tests for all questions --------- Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com> Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> * fix: update valid_for_secs (#13604) * fix: Fixes teams UI load issue (#13593) * fixes teams uiload issue * revert: yarn.lock --------- Co-authored-by: Udit Takkar <udit222001@gmail.com> * chore: upgrades boxyhq jackson (#13477) * chore: upgrades boxyhq jackson * Update jackson.ts reduces bundle size * Update yarn.lock * Update apps/web/next.config.js * Upgrades again --------- Co-authored-by: Keith Williams <keithwillcode@gmail.com> * upgrades copycat * feat: PLA-47 Reserve Slots Handler (#13607) * PLA-44 feat: Platform getPublicEvent (#13596) * chore: getPublicEvent * chore: fix * feat: booker atom bookings platform endpoints (#13613) * feat: handle new booking * wip * feat: handle recurring and instant booking * fixup! feat: handle recurring and instant booking --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com> * fix: v2 not running (#13636) * fix: import from platform-libraries instead of lib * fix: typescript error * fix: provide EventTypesRepository to SlotsService * fix: small change * Merge branch 'main' into platform * fix: make v2 run after user model changes (#13653) * feat: event type atom user connected and destination calendars endpoint (#13559) * extract connected calendars logic in re-usable platform-libraries function * extract connected calendars logic in re-usable platform-libraries function * extract connected calendars logic in re-usable platform-libraries function * calendars endpoints * enable calendars endpoint * calendars type form platform-libraries instead of lib * finish merging platform branch * fix dbWrite * fix: issue after merge platform * fix: new abstracted function export * change ee/overlay-calendars to ee/calendars endpoint * refactor: merge overlay service with calendars service * ts fix * body -> query * chore: Platform get schedule (#13696) * chore: get available slots * chore: infer event type isTeam * chore: nit * chore: Delete selected slot (#13692) Co-authored-by: Morgan Vernay <morgan@cal.com> * feat: v2 endpoint for availability atom timezones (#13687) * refactor: move cityTimezones handler to lib * test: incorrect timezone when creating oauth user * refactor: standardize time zone validation * feat: schedules/time-zones endpoint returning possible time zones * refactor: @IsValidTimezone load allowed timezones dynamically * chore: split timezones to its own trpc router --------- Co-authored-by: Morgan Vernay <morgan@cal.com> * fix after merge main * feat: platform useGetPublicEvent (#13752) * feat: platform useGetPublicEvent * chore: move public event type to libraries * chore: use const for query key * fixup CalendarBusyTimesInput optional params * feat: platform use get available slots (#13756) * feat: platform use get available slots * fixup! feat: platform use get available slots * feat: platform use get connected calendars (#13757) * chore: platform enable cors (#13774) * chore: enable cors * fixup! chore: enable cors * feat: platform use get calendars busy times (#13759) * fixup! feat: platform use get calendars busy times (#13759) * feat: platform use slots hooks (#13785) * feat: platform use slots hooks * fixup! feat: platform use slots hooks * fixup! fixup! feat: platform use slots hooks * fixup! fixup! fixup! feat: platform use slots hooks * fixup! fixup! fixup! fixup! feat: platform use slots hooks * fixup! feat: platform use slots hooks (#13785) * feat: platform use create boookings hooks (#13797) * feat: `Availability Settings` atom (#13762) * styling and updating the examples app * add custom hooks for availability settings atom * update default exports * minor updates * update cal provider to use react query * availability settings atom * dialog from shadcn * navbar for examples app * add select skeleton loader in calcom ui packages * refactors * define rtl for cal provider * fox trpc call to avoid merge conflicts * fix issues caused by merges * revert changes for rtl * hook to fetch every city timezones * implement useGetCityTimezones into timezone component * minor fix * update hooks * add props and intergrate into avaialibility component * add missing await * remove logs * add comments * invalidate queries after settled * toaster from shadcn * add toaster into cal provider * implement toast on event handlers * add classnames prop to accept custom styles * passing custom styles * update packages * remove comments * fix: styling * fix: useUpdateSchedule * fixup! fix: useUpdateSchedule * wip * feat: availability setting atom * fixup! feat: availability setting atom * fixup! fixup! feat: availability setting atom * fixup! fixup! fixup! feat: availability setting atom * fixup! fixup! fixup! fixup! feat: availability setting atom --------- Co-authored-by: Morgan Vernay <morgan@cal.com> * fix: atoms import * chore: lock * fix: types * fixup! fix: types * refactor: v2 user timezones (#13944) * fix: OAuth client form improvements (#13837) * rename app to apps * fix type errors, validate redirect url and other fixes * enable user to add multiple redirect uris * add id to input to make labels clickable * translations for oauth form * improve styles, add translations and add select all button for permissions * add permissions for profile read and write * fixes * nit * use `useFieldArray` for dynamic fields --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> * refactor: check organizationId primarily on user profile (#13969) * refactor: attach movedToProfile to user * refactor: check organizationId primarily on user profile * fix: resolve TS errors to build v2 for prod (#13858) * feat: atoms typescript build (#13864) * fix: atoms typescript build * refactor: dayjs * refactor: meticulous * refactor: use vite for type generation * tsconfig * feat: given user timezone update update default schedule timezone (#13981) * refactor: standardize schedules api response * fix: schedules e2e tests * refactor: use IsTimeZone class-validator instead of custom built one * feat: validate managed user timezone * feat: update default schedule when me schedule updated * refactor: standardize v2 api returned data (#13984) * feat: V2 api swagger (#13804) * feat: v2 API swagger docs * fix: query param not showing up in docs * latest swagger.json * refactor: remove scaffolded sample endpoint * separate internal endpoints * oauth-clients and oauth flow docs only in dev * oauth-clients and oauth flow docs only in dev * create oauth client response doc * responses for oauth-clients * document development only endpoints * chore: consume auth token on exchange (#13993) * feat: docker build for v2 (#13918) * fix: resolve TS errors to build v2 for prod * WIP: api v2 docker * arg env dockerfile * wip * Revert "wip" This reverts commit 232adf5b969ef004ae907f3e85b12c2715d9b6bc. * wip * wip * fix docker * fix docker * fix docker * docker * docker * expose port 80 * feat: oauth client permissions guard (#14020) * feat: Permissions guard * feat: Permissions guard * tests * feat: use permissions guard for event types * extra permissions test * fix tests * bokings, gcal, me and schedules permissions * chore: use availability settings atom and fix useTimezone platform hook (#13934) * replace availabiilty settings with atom component * add more props for custom styles * replace intl with dayjs * review feedback fixes * fix: v2 api typescript errors (#14059) * fix: start:prod script * fix: v2 me module dependencies (#14073) * feat: check v2 request origin (#14074) * feat: check v2 request origin * drive by: remove console log in test --------- Co-authored-by: Lauris <lauris@Lauriss-Laptop.local> * fix date overrides typing issues (#14091) Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> * fix: v2 e2e tests (#14088) * fix: v2 e2e tests * fix: yarn e2e tests run 1 after another * fix gcal test --------- Co-authored-by: Lauris <lauris@Lauriss-Laptop.local> * fix: availability atom and access token origin strategy * fix: availability atom get error from unknown * feat: booker atom platform wrapper (#14036) * feat: booker atom, fix hooks * wip * feat: booker atom platform wrapper * fix conflicts * fix: build v2 internal dependencies upon v2 build * fix typing of useDeleteOAuth * fix: isplatform timezone select improt booker event meta * fixup! Merge branch 'platform' into feat-booker-atom-platform * refactor * add video call event types * disable email and set managed user names --------- Co-authored-by: supalarry <lauris.skraucis@gmail.com> * update yarn.lock * Update .prettierignore * update post install * fix: post install * fix yarn lock * fix: post install * fix: types * feat: enable user control over timezone changes and helpers for query params (#14049) * helpers to get and set value of a query paramater * add event handlers and props to figure out the timezone preference of a user * make onTimeZoneChange handler optional * fixup * fix merge conflicts * fixup * revert changes * fixup * fixup --------- Co-authored-by: Alex van Andel <me@alexvanandel.com> * fix: usetimezone import * fix: prisma client import * fix: prisma client import * fix: prisma client import * remove post install using vite * ts fix attempt: pin prisma to same version * Revert "ts fix attempt: pin prisma to same version" This reverts commit 2c309f52e3a881e2dbda4185f45e77967bef6925. * resolve v2 prisma to calcom prisma * sync v2 and typescript deps * sync atoms package * lock file updat * fix: platform web components move out of pages folder * ui: allow experimental decorators to fix ts ci error * app-store: allow experimental decorators to fix ts ci error * web: allow experimental decorators to fix ts ci error * fix: github actions v2 api secrets * add license * mock useIsPlatform for tests * fix timezone select tests * mock useLocale * fix tests * fix tests * fix tests * fix github actions * chore: temporarily disable redis (#14142) * fix: only one migration file for platform * fix api v1 files location * fix: disable apiv2 test in ci --------- Co-authored-by: Ryukemeister <sahalrajiv-extc@atharvacoe.ac.in> Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com> Co-authored-by: Erik <erik@erosemberg.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: Pratik Kumar <70286186+Pratik-Kumar-621@users.noreply.github.com> Co-authored-by: Udit Takkar <udit222001@gmail.com> Co-authored-by: Samyabrata Maji <116789799+samyabrata-maji@users.noreply.github.com> Co-authored-by: Manpreet Singh <manpoffc@gmail.com> Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com> Co-authored-by: DmytroHryshyn <125881252+DmytroHryshyn@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Varun Prahlad Balani <varunprahladbalani@gmail.com> Co-authored-by: Mike Zhou <mikezhoudev@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Haran Rajkumar <haranrajkumar97@gmail.com> Co-authored-by: Harshith Pabbati <pabbatiharshith@gmail.com> Co-authored-by: Brendan Woodward <73412688+bwoody13@users.noreply.github.com> Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com> Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com> Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Anant Jain <75206987+anantJjain@users.noreply.github.com> Co-authored-by: Lauris <lauris@Lauriss-Laptop.local> Co-authored-by: supalarry <laurisskraucis@gmail.com>
1251 lines
43 KiB
TypeScript
1251 lines
43 KiB
TypeScript
import type { User as UserAuth } from "next-auth";
|
|
import { signOut, useSession } from "next-auth/react";
|
|
import dynamic from "next/dynamic";
|
|
import Link from "next/link";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import type { Dispatch, ReactElement, ReactNode, SetStateAction } from "react";
|
|
import React, { cloneElement, Fragment, useEffect, useMemo, useState } from "react";
|
|
import { Toaster } from "react-hot-toast";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
|
import UnconfirmedBookingBadge from "@calcom/features/bookings/UnconfirmedBookingBadge";
|
|
import ImpersonatingBanner, {
|
|
type ImpersonatingBannerProps,
|
|
} from "@calcom/features/ee/impersonation/components/ImpersonatingBanner";
|
|
import {
|
|
OrgUpgradeBanner,
|
|
type OrgUpgradeBannerProps,
|
|
} from "@calcom/features/ee/organizations/components/OrgUpgradeBanner";
|
|
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
|
import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem";
|
|
import { TeamsUpgradeBanner, type TeamsUpgradeBannerProps } from "@calcom/features/ee/teams/components";
|
|
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
|
import { KBarContent, KBarRoot, KBarTrigger } from "@calcom/features/kbar/Kbar";
|
|
import TimezoneChangeDialog from "@calcom/features/settings/TimezoneChangeDialog";
|
|
import AdminPasswordBanner, {
|
|
type AdminPasswordBannerProps,
|
|
} from "@calcom/features/users/components/AdminPasswordBanner";
|
|
import CalendarCredentialBanner, {
|
|
type CalendarCredentialBannerProps,
|
|
} from "@calcom/features/users/components/CalendarCredentialBanner";
|
|
import {
|
|
InvalidAppCredentialBanners,
|
|
type InvalidAppCredentialBannersProps,
|
|
} from "@calcom/features/users/components/InvalidAppCredentialsBanner";
|
|
import UserV2OptInBanner from "@calcom/features/users/components/UserV2OptInBanner";
|
|
import VerifyEmailBanner, {
|
|
type VerifyEmailBannerProps,
|
|
} from "@calcom/features/users/components/VerifyEmailBanner";
|
|
import classNames from "@calcom/lib/classNames";
|
|
import {
|
|
APP_NAME,
|
|
DESKTOP_APP_LINK,
|
|
JOIN_DISCORD,
|
|
ROADMAP,
|
|
WEBAPP_URL,
|
|
IS_VISUAL_REGRESSION_TESTING,
|
|
TOP_BANNER_HEIGHT,
|
|
ENABLE_PROFILE_SWITCHER,
|
|
} from "@calcom/lib/constants";
|
|
import { useFormbricks } from "@calcom/lib/formbricks-client";
|
|
import getBrandColours from "@calcom/lib/getBrandColours";
|
|
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
|
import { isKeyInObject } from "@calcom/lib/isKeyInObject";
|
|
import type { User } from "@calcom/prisma/client";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
import useEmailVerifyCheck from "@calcom/trpc/react/hooks/useEmailVerifyCheck";
|
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
|
import type { SVGComponent } from "@calcom/types/SVGComponent";
|
|
import {
|
|
Avatar,
|
|
Button,
|
|
ButtonOrLink,
|
|
Credits,
|
|
Dropdown,
|
|
DropdownItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuPortal,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
ErrorBoundary,
|
|
HeadSeo,
|
|
Logo,
|
|
showToast,
|
|
SkeletonText,
|
|
Tooltip,
|
|
useCalcomTheme,
|
|
} from "@calcom/ui";
|
|
import {
|
|
ArrowLeft,
|
|
ArrowRight,
|
|
BarChart,
|
|
Calendar,
|
|
ChevronDown,
|
|
Clock,
|
|
Copy,
|
|
Download,
|
|
ExternalLink,
|
|
FileText,
|
|
Grid,
|
|
HelpCircle,
|
|
Link as LinkIcon,
|
|
LogOut,
|
|
Map,
|
|
Moon,
|
|
MoreHorizontal,
|
|
Settings,
|
|
User as UserIcon,
|
|
Users,
|
|
Zap,
|
|
Check,
|
|
} from "@calcom/ui/components/icon";
|
|
import { Discord } from "@calcom/ui/components/icon/Discord";
|
|
|
|
import { useOrgBranding } from "../ee/organizations/context/provider";
|
|
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
|
|
import { TeamInviteBadge } from "./TeamInviteBadge";
|
|
|
|
// need to import without ssr to prevent hydration errors
|
|
const Tips = dynamic(() => import("@calcom/features/tips").then((mod) => mod.Tips), {
|
|
ssr: false,
|
|
});
|
|
|
|
/* TODO: Migate this */
|
|
|
|
export const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString();
|
|
|
|
export const ONBOARDING_NEXT_REDIRECT = {
|
|
redirect: {
|
|
permanent: false,
|
|
destination: "/getting-started",
|
|
},
|
|
} as const;
|
|
|
|
export const shouldShowOnboarding = (
|
|
user: Pick<User, "createdDate" | "completedOnboarding"> & {
|
|
organizationId: number | null;
|
|
}
|
|
) => {
|
|
return (
|
|
!user.completedOnboarding &&
|
|
!user.organizationId &&
|
|
dayjs(user.createdDate).isAfter(ONBOARDING_INTRODUCED_AT)
|
|
);
|
|
};
|
|
|
|
function useRedirectToLoginIfUnauthenticated(isPublic = false) {
|
|
const { data: session, status } = useSession();
|
|
const loading = status === "loading";
|
|
const router = useRouter();
|
|
useEffect(() => {
|
|
if (isPublic) {
|
|
return;
|
|
}
|
|
|
|
if (!loading && !session) {
|
|
const urlSearchParams = new URLSearchParams();
|
|
urlSearchParams.set("callbackUrl", `${WEBAPP_URL}${location.pathname}${location.search}`);
|
|
router.replace(`/auth/login?${urlSearchParams.toString()}`);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [loading, session, isPublic]);
|
|
|
|
return {
|
|
loading: loading && !session,
|
|
session,
|
|
};
|
|
}
|
|
|
|
type BannerTypeProps = {
|
|
teamUpgradeBanner: TeamsUpgradeBannerProps;
|
|
orgUpgradeBanner: OrgUpgradeBannerProps;
|
|
verifyEmailBanner: VerifyEmailBannerProps;
|
|
adminPasswordBanner: AdminPasswordBannerProps;
|
|
impersonationBanner: ImpersonatingBannerProps;
|
|
calendarCredentialBanner: CalendarCredentialBannerProps;
|
|
invalidAppCredentialBanners: InvalidAppCredentialBannersProps;
|
|
};
|
|
|
|
type BannerType = keyof BannerTypeProps;
|
|
|
|
type BannerComponent = {
|
|
[Key in BannerType]: (props: BannerTypeProps[Key]) => JSX.Element;
|
|
};
|
|
|
|
const BannerComponent: BannerComponent = {
|
|
teamUpgradeBanner: (props: TeamsUpgradeBannerProps) => <TeamsUpgradeBanner {...props} />,
|
|
orgUpgradeBanner: (props: OrgUpgradeBannerProps) => <OrgUpgradeBanner {...props} />,
|
|
verifyEmailBanner: (props: VerifyEmailBannerProps) => <VerifyEmailBanner {...props} />,
|
|
adminPasswordBanner: (props: AdminPasswordBannerProps) => <AdminPasswordBanner {...props} />,
|
|
impersonationBanner: (props: ImpersonatingBannerProps) => <ImpersonatingBanner {...props} />,
|
|
calendarCredentialBanner: (props: CalendarCredentialBannerProps) => <CalendarCredentialBanner {...props} />,
|
|
invalidAppCredentialBanners: (props: InvalidAppCredentialBannersProps) => (
|
|
<InvalidAppCredentialBanners {...props} />
|
|
),
|
|
};
|
|
|
|
function useRedirectToOnboardingIfNeeded() {
|
|
const router = useRouter();
|
|
const query = useMeQuery();
|
|
const user = query.data;
|
|
const flags = useFlagMap();
|
|
|
|
const { data: email } = useEmailVerifyCheck();
|
|
|
|
const needsEmailVerification = !email?.isVerified && flags["email-verification"];
|
|
|
|
const isRedirectingToOnboarding = user && shouldShowOnboarding(user);
|
|
|
|
useEffect(() => {
|
|
if (isRedirectingToOnboarding && !needsEmailVerification) {
|
|
router.replace("/getting-started");
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [isRedirectingToOnboarding, needsEmailVerification]);
|
|
|
|
return {
|
|
isRedirectingToOnboarding,
|
|
};
|
|
}
|
|
|
|
type allBannerProps = { [Key in BannerType]: BannerTypeProps[Key]["data"] };
|
|
|
|
const useBanners = () => {
|
|
const { data: getUserTopBanners, isPending } = trpc.viewer.getUserTopBanners.useQuery();
|
|
const { data: userSession } = useSession();
|
|
|
|
if (isPending || !userSession) return null;
|
|
|
|
const isUserInactiveAdmin = userSession?.user.role === "INACTIVE_ADMIN";
|
|
const userImpersonatedByUID = userSession?.user.impersonatedBy?.id;
|
|
|
|
const userSessionBanners = {
|
|
adminPasswordBanner: isUserInactiveAdmin ? userSession : null,
|
|
impersonationBanner: userImpersonatedByUID ? userSession : null,
|
|
};
|
|
|
|
const allBanners: allBannerProps = Object.assign({}, getUserTopBanners, userSessionBanners);
|
|
|
|
return allBanners;
|
|
};
|
|
|
|
const Layout = (props: LayoutProps) => {
|
|
const banners = useBanners();
|
|
|
|
const pageTitle = typeof props.heading === "string" && !props.title ? props.heading : props.title;
|
|
|
|
const bannersHeight = useMemo(() => {
|
|
const activeBanners =
|
|
banners &&
|
|
Object.entries(banners).filter(([_, value]) => {
|
|
return value && (!Array.isArray(value) || value.length > 0);
|
|
});
|
|
return (activeBanners?.length ?? 0) * TOP_BANNER_HEIGHT;
|
|
}, [banners]);
|
|
|
|
useFormbricks();
|
|
|
|
return (
|
|
<>
|
|
{!props.withoutSeo && (
|
|
<HeadSeo
|
|
title={pageTitle ?? APP_NAME}
|
|
description={props.description ?? props.subtitle?.toString() ?? ""}
|
|
/>
|
|
)}
|
|
<div>
|
|
<Toaster position="bottom-right" />
|
|
</div>
|
|
|
|
{/* todo: only run this if timezone is different */}
|
|
<TimezoneChangeDialog />
|
|
|
|
<div className="flex min-h-screen flex-col">
|
|
{banners && (
|
|
<div className="sticky top-0 z-10 w-full divide-y divide-black">
|
|
<UserV2OptInBanner />
|
|
{Object.keys(banners).map((key) => {
|
|
if (key === "teamUpgradeBanner") {
|
|
const Banner = BannerComponent[key];
|
|
return <Banner data={banners[key]} key={key} />;
|
|
} else if (key === "orgUpgradeBanner") {
|
|
const Banner = BannerComponent[key];
|
|
return <Banner data={banners[key]} key={key} />;
|
|
} else if (key === "verifyEmailBanner") {
|
|
const Banner = BannerComponent[key];
|
|
return <Banner data={banners[key]} key={key} />;
|
|
} else if (key === "adminPasswordBanner") {
|
|
const Banner = BannerComponent[key];
|
|
return <Banner data={banners[key]} key={key} />;
|
|
} else if (key === "impersonationBanner") {
|
|
const Banner = BannerComponent[key];
|
|
return <Banner data={banners[key]} key={key} />;
|
|
} else if (key === "calendarCredentialBanner") {
|
|
const Banner = BannerComponent[key];
|
|
return <Banner data={banners[key]} key={key} />;
|
|
} else if (key === "invalidAppCredentialBanners") {
|
|
const Banner = BannerComponent[key];
|
|
return <Banner data={banners[key]} key={key} />;
|
|
}
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-1" data-testid="dashboard-shell">
|
|
{props.SidebarContainer ? (
|
|
cloneElement(props.SidebarContainer, { bannersHeight })
|
|
) : (
|
|
<SideBarContainer bannersHeight={bannersHeight} />
|
|
)}
|
|
<div className="flex w-0 flex-1 flex-col">
|
|
<MainContainer {...props} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
type DrawerState = [isOpen: boolean, setDrawerOpen: Dispatch<SetStateAction<boolean>>];
|
|
|
|
export type LayoutProps = {
|
|
centered?: boolean;
|
|
title?: string;
|
|
description?: string;
|
|
heading?: ReactNode;
|
|
subtitle?: ReactNode;
|
|
headerClassName?: string;
|
|
children: ReactNode;
|
|
CTA?: ReactNode;
|
|
large?: boolean;
|
|
MobileNavigationContainer?: ReactNode;
|
|
SidebarContainer?: ReactElement;
|
|
TopNavContainer?: ReactNode;
|
|
drawerState?: DrawerState;
|
|
HeadingLeftIcon?: ReactNode;
|
|
backPath?: string | boolean; // renders back button to specified path
|
|
// use when content needs to expand with flex
|
|
flexChildrenContainer?: boolean;
|
|
isPublic?: boolean;
|
|
withoutMain?: boolean;
|
|
// Gives you the option to skip HeadSEO and render your own.
|
|
withoutSeo?: boolean;
|
|
// Gives the ability to include actions to the right of the heading
|
|
actions?: JSX.Element;
|
|
beforeCTAactions?: JSX.Element;
|
|
afterHeading?: ReactNode;
|
|
smallHeading?: boolean;
|
|
hideHeadingOnMobile?: boolean;
|
|
};
|
|
|
|
const useAppTheme = () => {
|
|
const { data: user } = useMeQuery();
|
|
const brandTheme = getBrandColours({
|
|
lightVal: user?.brandColor,
|
|
darkVal: user?.darkBrandColor,
|
|
});
|
|
useCalcomTheme(brandTheme);
|
|
useTheme(user?.appTheme);
|
|
};
|
|
|
|
const KBarWrapper = ({ children, withKBar = false }: { withKBar: boolean; children: React.ReactNode }) =>
|
|
withKBar ? (
|
|
<KBarRoot>
|
|
{children}
|
|
<KBarContent />
|
|
</KBarRoot>
|
|
) : (
|
|
<>{children}</>
|
|
);
|
|
|
|
const PublicShell = (props: LayoutProps) => {
|
|
const { status } = useSession();
|
|
return (
|
|
<KBarWrapper withKBar={status === "authenticated"}>
|
|
<Layout {...props} />
|
|
</KBarWrapper>
|
|
);
|
|
};
|
|
|
|
export default function Shell(props: LayoutProps) {
|
|
// if a page is unauthed and isPublic is true, the redirect does not happen.
|
|
useRedirectToLoginIfUnauthenticated(props.isPublic);
|
|
useRedirectToOnboardingIfNeeded();
|
|
useAppTheme();
|
|
|
|
return !props.isPublic ? (
|
|
<KBarWrapper withKBar>
|
|
<Layout {...props} />
|
|
</KBarWrapper>
|
|
) : (
|
|
<PublicShell {...props} />
|
|
);
|
|
}
|
|
|
|
interface UserDropdownProps {
|
|
small?: boolean;
|
|
}
|
|
|
|
function UserDropdown({ small }: UserDropdownProps) {
|
|
const { t } = useLocale();
|
|
const { data: user } = useMeQuery();
|
|
const utils = trpc.useContext();
|
|
const bookerUrl = useBookerUrl();
|
|
|
|
useEffect(() => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
//@ts-ignore
|
|
const Beacon = window.Beacon;
|
|
// window.Beacon is defined when user actually opens up HelpScout and username is available here. On every re-render update session info, so that it is always latest.
|
|
Beacon &&
|
|
Beacon("session-data", {
|
|
username: user?.username || "Unknown",
|
|
screenResolution: `${screen.width}x${screen.height}`,
|
|
});
|
|
});
|
|
const mutation = trpc.viewer.away.useMutation({
|
|
onMutate: async ({ away }) => {
|
|
await utils.viewer.me.cancel();
|
|
|
|
const previousValue = utils.viewer.me.getData();
|
|
|
|
if (previousValue) {
|
|
utils.viewer.me.setData(undefined, { ...previousValue, away });
|
|
}
|
|
|
|
return { previousValue };
|
|
},
|
|
onError: (_, __, context) => {
|
|
if (context?.previousValue) {
|
|
utils.viewer.me.setData(undefined, context.previousValue);
|
|
}
|
|
|
|
showToast(t("toggle_away_error"), "error");
|
|
},
|
|
onSettled() {
|
|
utils.viewer.me.invalidate();
|
|
},
|
|
});
|
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
|
|
const onHelpItemSelect = () => {
|
|
setHelpOpen(false);
|
|
setMenuOpen(false);
|
|
};
|
|
|
|
// Prevent rendering dropdown if user isn't available.
|
|
// We don't want to show nameless user.
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Dropdown open={menuOpen}>
|
|
<DropdownMenuTrigger asChild onClick={() => setMenuOpen((menuOpen) => !menuOpen)}>
|
|
<button
|
|
data-testid="user-dropdown-trigger-button"
|
|
className={classNames(
|
|
"hover:bg-emphasis todesktop:!bg-transparent group mx-0 flex w-full cursor-pointer appearance-none items-center rounded-full text-left outline-none transition focus:outline-none focus:ring-0 md:rounded-none lg:rounded",
|
|
small ? "p-2" : "px-2 py-1.5"
|
|
)}>
|
|
<span
|
|
className={classNames(
|
|
small ? "h-4 w-4" : "h-5 w-5 ltr:mr-2 rtl:ml-2",
|
|
"relative flex-shrink-0 rounded-full "
|
|
)}>
|
|
<Avatar
|
|
size={small ? "xs" : "xsm"}
|
|
imageSrc={`${user.avatarUrl || user.avatar}`}
|
|
alt={user.username || "Nameless User"}
|
|
className="overflow-hidden"
|
|
/>
|
|
<span
|
|
className={classNames(
|
|
"border-muted absolute -bottom-1 -right-1 rounded-full border bg-green-500",
|
|
user.away ? "bg-yellow-500" : "bg-green-500",
|
|
small ? "-bottom-0.5 -right-0.5 h-2.5 w-2.5" : "-bottom-0.5 -right-0 h-2 w-2"
|
|
)}
|
|
/>
|
|
</span>
|
|
{!small && (
|
|
<span className="flex flex-grow items-center gap-2">
|
|
<span className="w-24 flex-shrink-0 text-sm leading-none">
|
|
<span className="text-emphasis block truncate font-medium">
|
|
{user.name || "Nameless User"}
|
|
</span>
|
|
</span>
|
|
<ChevronDown
|
|
className="group-hover:text-subtle text-muted h-4 w-4 flex-shrink-0 rtl:mr-4"
|
|
aria-hidden="true"
|
|
/>
|
|
</span>
|
|
)}
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuPortal>
|
|
<FreshChatProvider>
|
|
<DropdownMenuContent
|
|
align="start"
|
|
onInteractOutside={() => {
|
|
setMenuOpen(false);
|
|
setHelpOpen(false);
|
|
}}
|
|
className="group overflow-hidden rounded-md">
|
|
{helpOpen ? (
|
|
<HelpMenuItem onHelpItemSelect={() => onHelpItemSelect()} />
|
|
) : (
|
|
<>
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
type="button"
|
|
StartIcon={(props) => (
|
|
<UserIcon className={classNames("text-default", props.className)} aria-hidden="true" />
|
|
)}
|
|
href="/settings/my-account/profile">
|
|
{t("my_profile")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
type="button"
|
|
StartIcon={(props) => (
|
|
<Settings className={classNames("text-default", props.className)} aria-hidden="true" />
|
|
)}
|
|
href="/settings/my-account/general">
|
|
{t("my_settings")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
type="button"
|
|
StartIcon={(props) => (
|
|
<Moon className={classNames("text-default", props.className)} aria-hidden="true" />
|
|
)}
|
|
href="/settings/my-account/out-of-office">
|
|
{t("out_of_office")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
StartIcon={() => <Discord className="text-default h-4 w-4" />}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
href={JOIN_DISCORD}>
|
|
{t("join_our_discord")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<DropdownItem StartIcon={Map} target="_blank" href={ROADMAP}>
|
|
{t("visit_roadmap")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
type="button"
|
|
StartIcon={(props) => <HelpCircle aria-hidden="true" {...props} />}
|
|
onClick={() => setHelpOpen(true)}>
|
|
{t("help")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="todesktop:hidden hidden lg:flex">
|
|
<DropdownItem StartIcon={Download} target="_blank" rel="noreferrer" href={DESKTOP_APP_LINK}>
|
|
{t("download_desktop_app")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
type="button"
|
|
StartIcon={(props) => <LogOut aria-hidden="true" {...props} />}
|
|
onClick={() => signOut({ callbackUrl: "/auth/logout" })}>
|
|
{t("sign_out")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</FreshChatProvider>
|
|
</DropdownMenuPortal>
|
|
</Dropdown>
|
|
);
|
|
}
|
|
|
|
export type NavigationItemType = {
|
|
name: string;
|
|
href: string;
|
|
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>;
|
|
target?: HTMLAnchorElement["target"];
|
|
badge?: React.ReactNode;
|
|
icon?: SVGComponent;
|
|
child?: NavigationItemType[];
|
|
pro?: true;
|
|
onlyMobile?: boolean;
|
|
onlyDesktop?: boolean;
|
|
isCurrent?: ({
|
|
item,
|
|
isChild,
|
|
pathname,
|
|
}: {
|
|
item: Pick<NavigationItemType, "href">;
|
|
isChild?: boolean;
|
|
pathname: string | null;
|
|
}) => boolean;
|
|
};
|
|
|
|
const requiredCredentialNavigationItems = ["Routing Forms"];
|
|
const MORE_SEPARATOR_NAME = "more";
|
|
|
|
const navigation: NavigationItemType[] = [
|
|
{
|
|
name: "event_types_page_title",
|
|
href: "/event-types",
|
|
icon: LinkIcon,
|
|
},
|
|
{
|
|
name: "bookings",
|
|
href: "/bookings/upcoming",
|
|
icon: Calendar,
|
|
badge: <UnconfirmedBookingBadge />,
|
|
isCurrent: ({ pathname }) => pathname?.startsWith("/bookings") ?? false,
|
|
},
|
|
{
|
|
name: "availability",
|
|
href: "/availability",
|
|
icon: Clock,
|
|
},
|
|
{
|
|
name: "teams",
|
|
href: "/teams",
|
|
icon: Users,
|
|
onlyDesktop: true,
|
|
badge: <TeamInviteBadge />,
|
|
},
|
|
{
|
|
name: "apps",
|
|
href: "/apps",
|
|
icon: Grid,
|
|
isCurrent: ({ pathname: path, item }) => {
|
|
// During Server rendering path is /v2/apps but on client it becomes /apps(weird..)
|
|
return (path?.startsWith(item.href) ?? false) && !(path?.includes("routing-forms/") ?? false);
|
|
},
|
|
child: [
|
|
{
|
|
name: "app_store",
|
|
href: "/apps",
|
|
isCurrent: ({ pathname: path, item }) => {
|
|
// During Server rendering path is /v2/apps but on client it becomes /apps(weird..)
|
|
return (
|
|
(path?.startsWith(item.href) ?? false) &&
|
|
!(path?.includes("routing-forms/") ?? false) &&
|
|
!(path?.includes("/installed") ?? false)
|
|
);
|
|
},
|
|
},
|
|
{
|
|
name: "installed_apps",
|
|
href: "/apps/installed/calendar",
|
|
isCurrent: ({ pathname: path }) =>
|
|
(path?.startsWith("/apps/installed/") ?? false) ||
|
|
(path?.startsWith("/v2/apps/installed/") ?? false),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: MORE_SEPARATOR_NAME,
|
|
href: "/more",
|
|
icon: MoreHorizontal,
|
|
},
|
|
{
|
|
name: "Routing Forms",
|
|
href: "/apps/routing-forms/forms",
|
|
icon: FileText,
|
|
isCurrent: ({ pathname }) => pathname?.startsWith("/apps/routing-forms/") ?? false,
|
|
},
|
|
{
|
|
name: "workflows",
|
|
href: "/workflows",
|
|
icon: Zap,
|
|
},
|
|
{
|
|
name: "insights",
|
|
href: "/insights",
|
|
icon: BarChart,
|
|
},
|
|
];
|
|
|
|
const moreSeparatorIndex = navigation.findIndex((item) => item.name === MORE_SEPARATOR_NAME);
|
|
// We create all needed navigation items for the different use cases
|
|
const { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems } = navigation.reduce<
|
|
Record<string, NavigationItemType[]>
|
|
>(
|
|
(items, item, index) => {
|
|
// We filter out the "more" separator in` desktop navigation
|
|
if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item);
|
|
// Items for mobile bottom navigation
|
|
if (index < moreSeparatorIndex + 1 && !item.onlyDesktop) {
|
|
items.mobileNavigationBottomItems.push(item);
|
|
} // Items for the "more" menu in mobile navigation
|
|
else {
|
|
items.mobileNavigationMoreItems.push(item);
|
|
}
|
|
return items;
|
|
},
|
|
{ desktopNavigationItems: [], mobileNavigationBottomItems: [], mobileNavigationMoreItems: [] }
|
|
);
|
|
|
|
const Navigation = () => {
|
|
return (
|
|
<nav className="mt-2 flex-1 md:px-2 lg:mt-4 lg:px-0">
|
|
{desktopNavigationItems.map((item) => (
|
|
<NavigationItem key={item.name} item={item} />
|
|
))}
|
|
<div className="text-subtle mt-0.5 lg:hidden">
|
|
<KBarTrigger />
|
|
</div>
|
|
</nav>
|
|
);
|
|
};
|
|
|
|
function useShouldDisplayNavigationItem(item: NavigationItemType) {
|
|
const flags = useFlagMap();
|
|
if (isKeyInObject(item.name, flags)) return flags[item.name];
|
|
return true;
|
|
}
|
|
|
|
const defaultIsCurrent: NavigationItemType["isCurrent"] = ({ isChild, item, pathname }) => {
|
|
return isChild ? item.href === pathname : item.href ? pathname?.startsWith(item.href) ?? false : false;
|
|
};
|
|
|
|
const NavigationItem: React.FC<{
|
|
index?: number;
|
|
item: NavigationItemType;
|
|
isChild?: boolean;
|
|
}> = (props) => {
|
|
const { item, isChild } = props;
|
|
const { t, isLocaleReady } = useLocale();
|
|
const pathname = usePathname();
|
|
const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent;
|
|
const current = isCurrent({ isChild: !!isChild, item, pathname });
|
|
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item);
|
|
|
|
if (!shouldDisplayNavigationItem) return null;
|
|
|
|
return (
|
|
<Fragment>
|
|
<Tooltip side="right" content={t(item.name)} className="lg:hidden">
|
|
<Link
|
|
data-test-id={item.name}
|
|
href={item.href}
|
|
aria-label={t(item.name)}
|
|
className={classNames(
|
|
"todesktop:py-[7px] text-default group flex items-center rounded-md px-2 py-1.5 text-sm font-medium transition",
|
|
item.child ? `[&[aria-current='page']]:!bg-transparent` : `[&[aria-current='page']]:bg-emphasis`,
|
|
isChild
|
|
? `[&[aria-current='page']]:text-emphasis [&[aria-current='page']]:bg-emphasis hidden h-8 pl-16 lg:flex lg:pl-11 ${
|
|
props.index === 0 ? "mt-0" : "mt-px"
|
|
}`
|
|
: "[&[aria-current='page']]:text-emphasis mt-0.5 text-sm",
|
|
isLocaleReady
|
|
? "hover:bg-subtle todesktop:[&[aria-current='page']]:bg-emphasis todesktop:hover:bg-transparent hover:text-emphasis"
|
|
: ""
|
|
)}
|
|
aria-current={current ? "page" : undefined}>
|
|
{item.icon && (
|
|
<item.icon
|
|
className="todesktop:!text-blue-500 mr-2 h-4 w-4 flex-shrink-0 rtl:ml-2 md:ltr:mx-auto lg:ltr:mr-2 [&[aria-current='page']]:text-inherit"
|
|
aria-hidden="true"
|
|
aria-current={current ? "page" : undefined}
|
|
/>
|
|
)}
|
|
{isLocaleReady ? (
|
|
<span
|
|
className="hidden w-full justify-between truncate text-ellipsis lg:flex"
|
|
data-testid={`${item.name}-test`}>
|
|
{t(item.name)}
|
|
{item.badge && item.badge}
|
|
</span>
|
|
) : (
|
|
<SkeletonText className="h-[20px] w-full" />
|
|
)}
|
|
</Link>
|
|
</Tooltip>
|
|
{item.child &&
|
|
isCurrent({ pathname, isChild, item }) &&
|
|
item.child.map((item, index) => <NavigationItem index={index} key={item.name} item={item} isChild />)}
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
function MobileNavigationContainer() {
|
|
const { status } = useSession();
|
|
if (status !== "authenticated") return null;
|
|
return <MobileNavigation />;
|
|
}
|
|
|
|
const MobileNavigation = () => {
|
|
const isEmbed = useIsEmbed();
|
|
|
|
return (
|
|
<>
|
|
<nav
|
|
className={classNames(
|
|
"pwa:pb-[max(0.625rem,env(safe-area-inset-bottom))] pwa:-mx-2 bg-muted border-subtle fixed bottom-0 left-0 z-30 flex w-full border-t bg-opacity-40 px-1 shadow backdrop-blur-md md:hidden",
|
|
isEmbed && "hidden"
|
|
)}>
|
|
{mobileNavigationBottomItems.map((item) => (
|
|
<MobileNavigationItem key={item.name} item={item} />
|
|
))}
|
|
</nav>
|
|
{/* add padding to content for mobile navigation*/}
|
|
<div className="block pt-12 md:hidden" />
|
|
</>
|
|
);
|
|
};
|
|
|
|
const MobileNavigationItem: React.FC<{
|
|
item: NavigationItemType;
|
|
isChild?: boolean;
|
|
}> = (props) => {
|
|
const { item, isChild } = props;
|
|
const pathname = usePathname();
|
|
const { t, isLocaleReady } = useLocale();
|
|
const isCurrent: NavigationItemType["isCurrent"] = item.isCurrent || defaultIsCurrent;
|
|
const current = isCurrent({ isChild: !!isChild, item, pathname });
|
|
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item);
|
|
|
|
if (!shouldDisplayNavigationItem) return null;
|
|
return (
|
|
<Link
|
|
key={item.name}
|
|
href={item.href}
|
|
className="[&[aria-current='page']]:text-emphasis hover:text-default text-muted relative my-2 min-w-0 flex-1 overflow-hidden rounded-md !bg-transparent p-1 text-center text-xs font-medium focus:z-10 sm:text-sm"
|
|
aria-current={current ? "page" : undefined}>
|
|
{item.badge && <div className="absolute right-1 top-1">{item.badge}</div>}
|
|
{item.icon && (
|
|
<item.icon
|
|
className="[&[aria-current='page']]:text-emphasis mx-auto mb-1 block h-5 w-5 flex-shrink-0 text-center text-inherit"
|
|
aria-hidden="true"
|
|
aria-current={current ? "page" : undefined}
|
|
/>
|
|
)}
|
|
{isLocaleReady ? <span className="block truncate">{t(item.name)}</span> : <SkeletonText />}
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
const MobileNavigationMoreItem: React.FC<{
|
|
item: NavigationItemType;
|
|
isChild?: boolean;
|
|
}> = (props) => {
|
|
const { item } = props;
|
|
const { t, isLocaleReady } = useLocale();
|
|
const shouldDisplayNavigationItem = useShouldDisplayNavigationItem(props.item);
|
|
|
|
if (!shouldDisplayNavigationItem) return null;
|
|
|
|
return (
|
|
<li className="border-subtle border-b last:border-b-0" key={item.name}>
|
|
<Link href={item.href} className="hover:bg-subtle flex items-center justify-between p-5 transition">
|
|
<span className="text-default flex items-center font-semibold ">
|
|
{item.icon && <item.icon className="h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3" aria-hidden="true" />}
|
|
{isLocaleReady ? t(item.name) : <SkeletonText />}
|
|
</span>
|
|
<ArrowRight className="text-subtle h-5 w-5" />
|
|
</Link>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
type SideBarContainerProps = {
|
|
bannersHeight: number;
|
|
};
|
|
|
|
type SideBarProps = {
|
|
bannersHeight: number;
|
|
user?: UserAuth | null;
|
|
};
|
|
|
|
function SideBarContainer({ bannersHeight }: SideBarContainerProps) {
|
|
const { status, data } = useSession();
|
|
|
|
// Make sure that Sidebar is rendered optimistically so that a refresh of pages when logged in have SideBar from the beginning.
|
|
// This improves the experience of refresh on app store pages(when logged in) which are SSG.
|
|
// Though when logged out, app store pages would temporarily show SideBar until session status is confirmed.
|
|
if (status !== "loading" && status !== "authenticated") return null;
|
|
return <SideBar bannersHeight={bannersHeight} user={data?.user} />;
|
|
}
|
|
|
|
function SideBar({ bannersHeight, user }: SideBarProps) {
|
|
const { t, isLocaleReady } = useLocale();
|
|
const orgBranding = useOrgBranding();
|
|
|
|
const publicPageUrl = useMemo(() => {
|
|
if (!user?.org?.id) return `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`;
|
|
const publicPageUrl = orgBranding?.slug ? getOrgFullOrigin(orgBranding.slug) : "";
|
|
return publicPageUrl;
|
|
}, [orgBranding?.slug, user?.username, user?.org?.id]);
|
|
|
|
const bottomNavItems: NavigationItemType[] = [
|
|
{
|
|
name: "view_public_page",
|
|
href: publicPageUrl,
|
|
icon: ExternalLink,
|
|
target: "__blank",
|
|
},
|
|
{
|
|
name: "copy_public_page_link",
|
|
href: "",
|
|
onClick: (e: { preventDefault: () => void }) => {
|
|
e.preventDefault();
|
|
navigator.clipboard.writeText(publicPageUrl);
|
|
showToast(t("link_copied"), "success");
|
|
},
|
|
icon: Copy,
|
|
},
|
|
{
|
|
name: "settings",
|
|
href: user?.org ? `/settings/organizations/profile` : "/settings/my-account/profile",
|
|
icon: Settings,
|
|
},
|
|
];
|
|
return (
|
|
<div className="relative">
|
|
<aside
|
|
style={{ maxHeight: `calc(100vh - ${bannersHeight}px)`, top: `${bannersHeight}px` }}
|
|
className="todesktop:!bg-transparent bg-muted border-muted fixed left-0 hidden h-full max-h-screen w-14 flex-col overflow-y-auto overflow-x-hidden border-r md:sticky md:flex lg:w-56 lg:px-3">
|
|
<div className="flex h-full flex-col justify-between py-3 lg:pt-4">
|
|
<header className="todesktop:-mt-3 todesktop:flex-col-reverse todesktop:[-webkit-app-region:drag] items-center justify-between md:hidden lg:flex">
|
|
{orgBranding ? (
|
|
!ENABLE_PROFILE_SWITCHER ? (
|
|
<Link href="/settings/organizations/profile" className="w-full px-1.5">
|
|
<div className="flex items-center gap-2 font-medium">
|
|
<Avatar
|
|
alt={`${orgBranding.name} logo`}
|
|
imageSrc={`${orgBranding.fullDomain}/org/${orgBranding.slug}/avatar.png`}
|
|
size="xsm"
|
|
/>
|
|
<p className="text line-clamp-1 text-sm">
|
|
<span>{orgBranding.name}</span>
|
|
</p>
|
|
</div>
|
|
</Link>
|
|
) : (
|
|
<ProfileDropdown />
|
|
)
|
|
) : (
|
|
<div data-testid="user-dropdown-trigger" className="todesktop:mt-4 w-full">
|
|
<span className="hidden lg:inline">
|
|
<UserDropdown />
|
|
</span>
|
|
<span className="hidden md:inline lg:hidden">
|
|
<UserDropdown small />
|
|
</span>
|
|
</div>
|
|
)}
|
|
<div className="flex justify-end rtl:space-x-reverse">
|
|
<button
|
|
color="minimal"
|
|
onClick={() => window.history.back()}
|
|
className="todesktop:block hover:text-emphasis text-subtle group hidden text-sm font-medium">
|
|
<ArrowLeft className="group-hover:text-emphasis text-subtle h-4 w-4 flex-shrink-0" />
|
|
</button>
|
|
<button
|
|
color="minimal"
|
|
onClick={() => window.history.forward()}
|
|
className="todesktop:block hover:text-emphasis text-subtle group hidden text-sm font-medium">
|
|
<ArrowRight className="group-hover:text-emphasis text-subtle h-4 w-4 flex-shrink-0" />
|
|
</button>
|
|
{!!orgBranding && (
|
|
<div data-testid="user-dropdown-trigger" className="flex items-center">
|
|
<UserDropdown small />
|
|
</div>
|
|
)}
|
|
<KBarTrigger />
|
|
</div>
|
|
</header>
|
|
|
|
{/* logo icon for tablet */}
|
|
<Link href="/event-types" className="text-center md:inline lg:hidden">
|
|
<Logo small icon />
|
|
</Link>
|
|
|
|
<Navigation />
|
|
</div>
|
|
|
|
<div>
|
|
<Tips />
|
|
{bottomNavItems.map(({ icon: Icon, ...item }, index) => (
|
|
<Tooltip side="right" content={t(item.name)} className="lg:hidden" key={item.name}>
|
|
<ButtonOrLink
|
|
id={item.name}
|
|
href={item.href || undefined}
|
|
aria-label={t(item.name)}
|
|
target={item.target}
|
|
className={classNames(
|
|
"text-left",
|
|
"[&[aria-current='page']]:bg-emphasis text-default justify-right group flex items-center rounded-md px-2 py-1.5 text-sm font-medium transition",
|
|
"[&[aria-current='page']]:text-emphasis mt-0.5 w-full text-sm",
|
|
isLocaleReady ? "hover:bg-emphasis hover:text-emphasis" : "",
|
|
index === 0 && "mt-3"
|
|
)}
|
|
onClick={item.onClick}>
|
|
{!!Icon && (
|
|
<Icon
|
|
className={classNames(
|
|
"h-4 w-4 flex-shrink-0 [&[aria-current='page']]:text-inherit",
|
|
"me-3 md:mx-auto lg:ltr:mr-2 lg:rtl:ml-2"
|
|
)}
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
{isLocaleReady ? (
|
|
<span className="hidden w-full justify-between lg:flex">
|
|
<div className="flex">{t(item.name)}</div>
|
|
</span>
|
|
) : (
|
|
<SkeletonText className="h-[20px] w-full" />
|
|
)}
|
|
</ButtonOrLink>
|
|
</Tooltip>
|
|
))}
|
|
{!IS_VISUAL_REGRESSION_TESTING && <Credits />}
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ShellMain(props: LayoutProps) {
|
|
const router = useRouter();
|
|
const { isLocaleReady } = useLocale();
|
|
|
|
return (
|
|
<>
|
|
{(props.heading || !!props.backPath) && (
|
|
<div
|
|
className={classNames(
|
|
"flex items-center md:mb-6 md:mt-0",
|
|
props.smallHeading ? "lg:mb-7" : "lg:mb-8",
|
|
props.hideHeadingOnMobile ? "mb-0" : "mb-6"
|
|
)}>
|
|
{!!props.backPath && (
|
|
<Button
|
|
variant="icon"
|
|
size="sm"
|
|
color="minimal"
|
|
onClick={() =>
|
|
typeof props.backPath === "string" ? router.push(props.backPath as string) : router.back()
|
|
}
|
|
StartIcon={ArrowLeft}
|
|
aria-label="Go Back"
|
|
className="rounded-md ltr:mr-2 rtl:ml-2"
|
|
/>
|
|
)}
|
|
{props.heading && (
|
|
<header
|
|
className={classNames(props.large && "py-8", "flex w-full max-w-full items-center truncate")}>
|
|
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
|
|
<div
|
|
className={classNames("w-full truncate ltr:mr-4 rtl:ml-4 md:block", props.headerClassName)}>
|
|
{props.heading && (
|
|
<h3
|
|
className={classNames(
|
|
"font-cal max-w-28 sm:max-w-72 md:max-w-80 text-emphasis inline truncate text-lg font-semibold tracking-wide sm:text-xl md:block xl:max-w-full",
|
|
props.smallHeading ? "text-base" : "text-xl",
|
|
props.hideHeadingOnMobile && "hidden"
|
|
)}>
|
|
{!isLocaleReady ? <SkeletonText invisible /> : props.heading}
|
|
</h3>
|
|
)}
|
|
{props.subtitle && (
|
|
<p className="text-default hidden text-sm md:block" data-testid="subtitle">
|
|
{!isLocaleReady ? <SkeletonText invisible /> : props.subtitle}
|
|
</p>
|
|
)}
|
|
</div>
|
|
{props.beforeCTAactions}
|
|
{props.CTA && (
|
|
<div
|
|
className={classNames(
|
|
props.backPath
|
|
? "relative"
|
|
: "pwa:bottom-[max(7rem,_calc(5rem_+_env(safe-area-inset-bottom)))] fixed bottom-20 z-40 ltr:right-4 rtl:left-4 md:z-auto md:ltr:right-0 md:rtl:left-0",
|
|
"flex-shrink-0 [-webkit-app-region:no-drag] md:relative md:bottom-auto md:right-auto"
|
|
)}>
|
|
{isLocaleReady && props.CTA}
|
|
</div>
|
|
)}
|
|
{props.actions && props.actions}
|
|
</header>
|
|
)}
|
|
</div>
|
|
)}
|
|
{props.afterHeading && <>{props.afterHeading}</>}
|
|
<div className={classNames(props.flexChildrenContainer && "flex flex-1 flex-col")}>
|
|
{props.children}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function MainContainer({
|
|
MobileNavigationContainer: MobileNavigationContainerProp = <MobileNavigationContainer />,
|
|
TopNavContainer: TopNavContainerProp = <TopNavContainer />,
|
|
...props
|
|
}: LayoutProps) {
|
|
return (
|
|
<main className="bg-default relative z-0 flex-1 focus:outline-none">
|
|
{/* show top navigation for md and smaller (tablet and phones) */}
|
|
{TopNavContainerProp}
|
|
<div className="max-w-full px-2 py-4 lg:px-6">
|
|
<ErrorBoundary>
|
|
{!props.withoutMain ? <ShellMain {...props}>{props.children}</ShellMain> : props.children}
|
|
</ErrorBoundary>
|
|
{/* show bottom navigation for md and smaller (tablet and phones) on pages where back button doesn't exist */}
|
|
{!props.backPath ? MobileNavigationContainerProp : null}
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function TopNavContainer() {
|
|
const { status } = useSession();
|
|
if (status !== "authenticated") return null;
|
|
return <TopNav />;
|
|
}
|
|
|
|
function TopNav() {
|
|
const isEmbed = useIsEmbed();
|
|
const { t } = useLocale();
|
|
return (
|
|
<>
|
|
<nav
|
|
style={isEmbed ? { display: "none" } : {}}
|
|
className="bg-muted border-subtle sticky top-0 z-40 flex w-full items-center justify-between border-b bg-opacity-50 px-4 py-1.5 backdrop-blur-lg sm:p-4 md:hidden">
|
|
<Link href="/event-types">
|
|
<Logo />
|
|
</Link>
|
|
<div className="flex items-center gap-2 self-center">
|
|
<span className="hover:bg-muted hover:text-emphasis text-default group flex items-center rounded-full text-sm font-medium lg:hidden">
|
|
<KBarTrigger />
|
|
</span>
|
|
<button className="hover:bg-muted hover:text-subtle text-muted rounded-full p-1 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
|
<span className="sr-only">{t("settings")}</span>
|
|
<Link href="/settings/my-account/profile">
|
|
<Settings className="text-default h-4 w-4" aria-hidden="true" />
|
|
</Link>
|
|
</button>
|
|
<UserDropdown small />
|
|
</div>
|
|
</nav>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export const MobileNavigationMoreItems = () => (
|
|
<ul className="border-subtle mt-2 rounded-md border">
|
|
{mobileNavigationMoreItems.map((item) => (
|
|
<MobileNavigationMoreItem key={item.name} item={item} />
|
|
))}
|
|
</ul>
|
|
);
|
|
|
|
function ProfileDropdown() {
|
|
const { update, data: sessionData } = useSession();
|
|
const { data } = trpc.viewer.me.useQuery();
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
|
|
if (!data || !ENABLE_PROFILE_SWITCHER || !sessionData) {
|
|
return null;
|
|
}
|
|
const options = data.profiles.map((profile) => {
|
|
let label;
|
|
if (profile.organization) {
|
|
label = profile.organization.name;
|
|
} else {
|
|
label = sessionData.user.name;
|
|
}
|
|
|
|
return {
|
|
label,
|
|
value: profile.upId,
|
|
};
|
|
});
|
|
|
|
const currentOption = options.find((option) => option.value === sessionData.upId) || options[0];
|
|
|
|
return (
|
|
<Dropdown open={menuOpen}>
|
|
<DropdownMenuTrigger asChild onClick={() => setMenuOpen((menuOpen) => !menuOpen)}>
|
|
<button
|
|
data-testid="user-dropdown-trigger-button"
|
|
className={classNames(
|
|
"hover:bg-emphasis todesktop:!bg-transparent group mx-0 flex w-full cursor-pointer appearance-none items-center rounded-full px-2 py-1.5 text-left outline-none transition focus:outline-none focus:ring-0 md:rounded-none lg:rounded"
|
|
)}>
|
|
<span className="flex w-full flex-grow items-center justify-around gap-2 text-sm font-medium leading-none">
|
|
<Avatar alt={currentOption.label || ""} size="xsm" />
|
|
<span className="block w-20 overflow-hidden overflow-ellipsis whitespace-nowrap">
|
|
{currentOption.label}
|
|
</span>
|
|
<ChevronDown
|
|
className="group-hover:text-subtle text-muted h-4 w-4 flex-shrink-0 rtl:mr-4"
|
|
aria-hidden="true"
|
|
/>
|
|
</span>
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuPortal>
|
|
<DropdownMenuContent
|
|
align="start"
|
|
onInteractOutside={() => {
|
|
setMenuOpen(false);
|
|
}}
|
|
className="min-w-56 hariom group overflow-hidden rounded-md">
|
|
<DropdownMenuItem className="p-3 uppercase">
|
|
<span>Switch to</span>
|
|
</DropdownMenuItem>
|
|
{options.map((option) => {
|
|
const isSelected = currentOption.value === option.value;
|
|
return (
|
|
<DropdownMenuItem
|
|
key={option.value}
|
|
onClick={() => {
|
|
setMenuOpen(false);
|
|
if (isSelected) return;
|
|
update({
|
|
upId: option.value,
|
|
}).then(() => {
|
|
window.location.reload();
|
|
});
|
|
}}
|
|
className={classNames("flex w-full", isSelected ? "bg-subtle text-emphasis" : "")}>
|
|
<DropdownItem
|
|
type="button"
|
|
childrenClassName={classNames("flex w-full justify-between items-center")}>
|
|
<span>
|
|
<Avatar alt={option.label || ""} size="xsm" />
|
|
<span className="ml-2">{option.label}</span>
|
|
</span>
|
|
{isSelected ? <Check className="ml-2 inline h-4 w-4" aria-hidden="true" /> : null}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
|
|
{/* <DropdownMenuSeparator /> */}
|
|
</DropdownMenuContent>
|
|
</DropdownMenuPortal>
|
|
</Dropdown>
|
|
);
|
|
}
|