{"version":3,"file":"server-entry.mjs","sources":["../../common/resources/client/workspace/active-workspace-id.ts","../../common/resources/client/utils/urls/is-absolute-url.ts","../../common/resources/client/utils/http/error-status-is.ts","../../common/resources/client/http/query-client.ts","../../common/resources/client/core/bootstrap-data/use-backend-bootstrap-data.ts","../../common/resources/client/core/settings/site-config-context.ts","../../common/resources/client/i18n/message.ts","../../resources/client/admin/verts/file-preview.png","../../resources/client/admin/verts/drive.png","../../resources/client/admin/verts/landing-top.png","../../common/resources/client/icons/svg-icon.tsx","../../common/resources/client/icons/create-svg-icon.tsx","../../common/resources/client/icons/material/GroupAdd.tsx","../../common/resources/client/icons/material/People.tsx","../../common/resources/client/icons/material/FileDownloadDone.tsx","../../common/resources/client/utils/urls/get-asset-url.ts","../../common/resources/client/ui/images/svg-image/svg-image.tsx","../../common/resources/client/ui/images/mixed-image.tsx","../../common/resources/client/ui/buttons/button-size.ts","../../common/resources/client/ui/buttons/get-shared-button-style.ts","../../common/resources/client/utils/dom/create-event-handler.ts","../../common/resources/client/ui/buttons/button-base.tsx","../../common/resources/client/ui/buttons/button.tsx","../../common/resources/client/utils/shallow-equal.ts","../../common/resources/client/core/bootstrap-data/bootstrap-data-context.ts","../../common/resources/client/i18n/selected-locale.ts","../../common/resources/client/i18n/use-user-timezone.ts","../../common/resources/client/i18n/handle-plural-message.tsx","../../common/resources/client/i18n/trans.tsx","../../common/resources/client/i18n/formatted-relative-time.tsx","../../common/resources/client/notifications/notification-line.tsx","../../common/resources/client/notifications/dialog/requests/user-notifications.ts","../../common/resources/client/ui/toast/toast-timer.ts","../../common/resources/client/ui/toast/toast-store.ts","../../common/resources/client/ui/toast/toast.ts","../../common/resources/client/utils/http/get-axios-error-message.ts","../../common/resources/client/utils/http/show-http-error-toast.ts","../../common/resources/client/notifications/requests/use-mark-notifications-as-read.ts","../../common/resources/client/utils/hooks/use-navigate.ts","../../common/resources/client/core/settings/use-settings.ts","../../common/resources/client/notifications/notification-list.tsx","../../common/resources/client/uploads/file-type-icon/icons/default-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/audio-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/video-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/text-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/pdf-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/archive-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/folder-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/image-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/power-point-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/word-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/spreadsheet-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/icons/shared-folder-file-icon.tsx","../../common/resources/client/uploads/file-type-icon/file-type-icon.tsx","../../resources/client/drive/notifications/file-entry-shared-notification-renderer.tsx","../../resources/client/site-config.tsx","../../common/resources/client/workspace/requests/workspace-query-keys.ts","../../common/resources/client/workspace/user-workspaces.ts","../../common/resources/client/utils/dom/is-ssr.ts","../../common/resources/client/utils/hooks/use-cookie.ts","../../common/resources/client/workspace/active-workspace-id-context.tsx","../../common/resources/client/workspace/requests/join-workspace.ts","../../common/resources/client/workspace/requests/delete-invite.ts","../../common/resources/client/ui/overlays/dialog/dialog-context.ts","../../common/resources/client/workspace/notifications/workspace-invite-notification-renderer.tsx","../../common/resources/client/core/settings/base-site-config.ts","../../common/resources/client/core/root-el.ts","../../common/resources/client/ui/themes/utils/set-theme-color.ts","../../common/resources/client/ui/themes/utils/apply-theme-to-dom.ts","../../common/resources/client/ui/themes/theme-selector-context.ts","../../common/resources/client/core/theme-provider.tsx","../../common/resources/client/core/bootstrap-data/bootstrap-data-provider.tsx","../../common/resources/client/core/common-provider.tsx","../../common/resources/client/utils/hooks/local-storage.ts","../../common/resources/client/auth/use-auth.ts","../../common/resources/client/ui/buttons/icon-button.tsx","../../common/resources/client/icons/material/Close.tsx","../../common/resources/client/i18n/mixed-text.tsx","../../common/resources/client/icons/material/ErrorOutline.tsx","../../common/resources/client/icons/material/CheckCircle.tsx","../../common/resources/client/utils/number/clamp.ts","../../common/resources/client/i18n/use-number-formatter.ts","../../common/resources/client/ui/progress/progress-circle.tsx","../../common/resources/client/ui/toast/toast-container.tsx","../../common/resources/client/auth/ui/use-user.ts","../../common/resources/client/auth/ui/email-verification-page/mail-sent.svg","../../common/resources/client/auth/requests/use-resend-verification-email.ts","../../common/resources/client/ui/themes/use-is-dark-mode.ts","../../common/resources/client/admin/appearance/commands/use-appearance-editor-mode.ts","../../common/resources/client/auth/requests/logout.ts","../../common/resources/client/auth/ui/email-verification-page/email-verification-page.tsx","../../common/resources/client/ui/overlays/store/dialog-store.ts","../../common/resources/client/ui/overlays/floating-position.ts","../../common/resources/client/utils/hooks/use-media-query.ts","../../common/resources/client/utils/hooks/is-mobile-media-query.ts","../../common/resources/client/ui/overlays/popover-animation.ts","../../common/resources/client/ui/overlays/use-overlay-viewport.ts","../../common/resources/client/ui/overlays/popover.tsx","../../common/resources/client/ui/animation/opacity-animation.ts","../../common/resources/client/ui/overlays/underlay.tsx","../../common/resources/client/ui/overlays/tray.tsx","../../common/resources/client/ui/overlays/modal.tsx","../../common/resources/client/ui/forms/listbox/section.tsx","../../common/resources/client/ui/forms/listbox/build-listbox-collection.ts","../../common/resources/client/ui/forms/listbox/use-listbox.ts","../../common/resources/client/ui/forms/listbox/listbox-context.ts","../../common/resources/client/utils/hooks/is-mobile-device.ts","../../common/resources/client/ui/forms/listbox/listbox.tsx","../../common/resources/client/icons/material/Check.tsx","../../common/resources/client/ui/list/list-item-base.tsx","../../common/resources/client/ui/forms/listbox/item.tsx","../../common/resources/client/ui/forms/listbox/use-listbox-keyboard-navigation.ts","../../common/resources/client/i18n/use-collator.ts","../../common/resources/client/ui/forms/listbox/use-type-select.ts","../../common/resources/client/icons/material/Search.tsx","../../common/resources/client/ui/forms/input-field/get-input-field-class-names.ts","../../common/resources/client/ui/forms/input-field/adornment.tsx","../../common/resources/client/utils/objects/remove-empty-values-from-object.ts","../../common/resources/client/ui/forms/input-field/field.tsx","../../common/resources/client/ui/focus/use-auto-focus.ts","../../common/resources/client/ui/forms/input-field/use-field.ts","../../common/resources/client/ui/forms/input-field/text-field/text-field.tsx","../../common/resources/client/ui/navigation/menu/menu-trigger.tsx","../../common/resources/client/ui/navigation/menu/context-menu.tsx","../../common/resources/client/utils/hooks/use-callback-ref.ts","../../common/resources/client/ui/overlays/dialog/dialog-trigger.tsx","../../common/resources/client/ui/overlays/store/dialog-store-outlet.tsx","../../common/resources/client/admin/appearance/commands/appearance-listener.tsx","../../common/resources/client/menus/use-custom-menu.ts","../../common/resources/client/menus/custom-menu.tsx","../../common/resources/client/ui/cookie-notice/cookie-notice.tsx","../../common/resources/client/auth/guards/guest-route.tsx","../../common/resources/client/ui/forms/form.tsx","../../common/resources/client/ui/buttons/external-link.tsx","../../common/resources/client/errors/on-form-query-error.ts","../../common/resources/client/auth/requests/use-register.ts","../../common/resources/client/auth/requests/connect-social-with-password.ts","../../common/resources/client/i18n/use-trans.ts","../../common/resources/client/ui/overlays/dialog/dismiss-button.tsx","../../common/resources/client/ui/overlays/dialog/dialog.tsx","../../common/resources/client/ui/overlays/dialog/dialog-header.tsx","../../common/resources/client/ui/overlays/dialog/dialog-body.tsx","../../common/resources/client/ui/overlays/dialog/dialog-footer.tsx","../../common/resources/client/auth/requests/disconnect-social.ts","../../common/resources/client/auth/requests/use-social-login.ts","../../common/resources/client/icons/social/google.tsx","../../common/resources/client/icons/social/facebook.tsx","../../common/resources/client/icons/social/twitter.tsx","../../common/resources/client/icons/social/envato.tsx","../../common/resources/client/auth/ui/social-auth-section.tsx","../../common/resources/client/auth/ui/auth-layout/auth-layout-footer.tsx","../../common/resources/client/auth/ui/auth-layout/auth-bg.svg","../../common/resources/client/auth/ui/auth-layout/auth-layout.tsx","../../common/resources/client/icons/material/CheckBoxOutlineBlank.tsx","../../common/resources/client/ui/forms/toggle/checkbox-filled-icon.tsx","../../common/resources/client/ui/forms/toggle/indeterminate-checkbox-filled-icon.tsx","../../common/resources/client/ui/forms/toggle/checkbox.tsx","../../common/resources/client/utils/http/lazy-loader.ts","../../common/resources/client/recaptcha/use-recaptcha.ts","../../common/resources/client/seo/helmet.tsx","../../common/resources/client/seo/static-page-title.tsx","../../common/resources/client/auth/ui/register-page.tsx","../../common/resources/client/custom-page/use-custom-page.ts","../../common/resources/client/icons/material/Notifications.tsx","../../common/resources/client/ui/badge/badge.tsx","../../common/resources/client/icons/material/DoneAll.tsx","../../common/resources/client/ui/images/illustrated-message.tsx","../../common/resources/client/notifications/empty-state/notify.svg","../../common/resources/client/notifications/empty-state/notification-empty-state-message.tsx","../../common/resources/client/icons/material/Settings.tsx","../../common/resources/client/notifications/dialog/notification-dialog-trigger.tsx","../../common/resources/client/icons/material/Menu.tsx","../../common/resources/client/icons/material/Person.tsx","../../common/resources/client/icons/material/ArrowDropDown.tsx","../../common/resources/client/icons/material/Payments.tsx","../../common/resources/client/icons/material/AccountCircle.tsx","../../common/resources/client/icons/material/DarkMode.tsx","../../common/resources/client/icons/material/LightMode.tsx","../../common/resources/client/icons/material/ExitToApp.tsx","../../common/resources/client/ui/navigation/navbar/navbar-auth-menu.tsx","../../common/resources/client/ui/navigation/navbar/navbar-auth-user.tsx","../../common/resources/client/ui/navigation/navbar/navbar-auth-buttons.tsx","../../common/resources/client/ui/themes/use-dark-theme-variables.ts","../../common/resources/client/ui/navigation/navbar/logo.tsx","../../common/resources/client/ui/navigation/navbar/navbar.tsx","../../common/resources/client/http/value-lists.ts","../../common/resources/client/icons/material/Language.tsx","../../common/resources/client/icons/material/KeyboardArrowDown.tsx","../../common/resources/client/i18n/change-locale.ts","../../common/resources/client/i18n/locale-switcher.tsx","../../common/resources/client/icons/material/Lightbulb.tsx","../../common/resources/client/ui/footer/footer.tsx","../../common/resources/client/text-editor/highlight/highlight-code.ts","../../common/resources/client/custom-page/custom-page-body.tsx","../../common/resources/client/seo/default-meta-tags.tsx","../../common/resources/client/http/page-meta-tags.tsx","../../common/resources/client/ui/progress/full-page-loader.tsx","../../common/resources/client/ui/not-found-page/404-1.png","../../common/resources/client/ui/not-found-page/404-2.png","../../common/resources/client/ui/not-found-page/not-found-page.tsx","../../common/resources/client/icons/material/Error.tsx","../../common/resources/client/errors/page-error-message.tsx","../../common/resources/client/utils/hooks/use-spin-delay.ts","../../common/resources/client/http/page-status.tsx","../../common/resources/client/custom-page/custom-page-layout.tsx","../../common/resources/client/auth/requests/use-login.ts","../../common/resources/client/auth/ui/two-factor/requests/use-two-factor-challenge.ts","../../common/resources/client/auth/ui/two-factor/two-factor-challenge-page.tsx","../../common/resources/client/auth/ui/login-page.tsx","../../common/resources/client/auth/ui/login-page-wrapper.tsx","../../common/resources/client/ui/dynamic-homepage.tsx","../../common/resources/client/admin/ads/ad-host.tsx","../../resources/client/landing/landing-page.tsx","../../common/resources/client/auth/guards/auth-route.tsx","../../common/resources/client/auth/ui/account-settings/account-settings-panel.tsx","../../common/resources/client/ui/list/list.tsx","../../common/resources/client/icons/material/Login.tsx","../../common/resources/client/icons/material/Lock.tsx","../../common/resources/client/icons/material/PhonelinkLock.tsx","../../common/resources/client/icons/material/Api.tsx","../../common/resources/client/icons/material/Dangerous.tsx","../../common/resources/client/icons/material/Devices.tsx","../../common/resources/client/auth/ui/account-settings/account-settings-sidenav.tsx","../../common/resources/client/auth/ui/account-settings/social-login-panel.tsx","../../common/resources/client/auth/ui/account-settings/basic-info-panel/update-account-details.ts","../../common/resources/client/auth/ui/account-settings/avatar/upload-avatar.ts","../../common/resources/client/auth/ui/account-settings/avatar/remove-avatar.ts","../../common/resources/client/uploads/uploader/strategy/s3-multipart-upload.ts","../../common/resources/client/uploads/uploader/strategy/tus-upload.ts","../../common/resources/client/uploads/types/backend-metadata.ts","../../common/resources/client/uploads/uploader/strategy/s3-upload.ts","../../common/resources/client/uploads/uploader/strategy/axios-upload.ts","../../common/resources/client/uploads/utils/pretty-bytes.ts","../../common/resources/client/uploads/uploader/validate-upload.ts","../../common/resources/client/uploads/uploader/progress-timeout.ts","../../common/resources/client/uploads/uploader/start-uploading.tsx","../../common/resources/client/uploads/utils/extension-from-filename.ts","../../common/resources/client/uploads/utils/get-file-mime.ts","../../common/resources/client/uploads/uploaded-file.ts","../../common/resources/client/uploads/uploader/create-file-upload.ts","../../common/resources/client/uploads/uploader/file-upload-store.ts","../../common/resources/client/uploads/uploader/file-upload-provider.tsx","../../common/resources/client/uploads/utils/create-upload-input.ts","../../common/resources/client/uploads/utils/open-upload-window.ts","../../common/resources/client/uploads/requests/delete-file-entries.ts","../../common/resources/client/uploads/uploader/use-active-upload.ts","../../common/resources/client/uploads/types/upload-input-config.ts","../../common/resources/client/ui/progress/progress-bar-base.tsx","../../common/resources/client/ui/progress/progress-bar.tsx","../../common/resources/client/icons/material/AddAPhoto.tsx","../../common/resources/client/auth/ui/account-settings/avatar/avatar-placeholder-icon.tsx","../../common/resources/client/ui/images/image-selector.tsx","../../common/resources/client/auth/ui/account-settings/basic-info-panel/basic-info-panel.tsx","../../common/resources/client/auth/ui/account-settings/change-password-panel/use-update-password.ts","../../common/resources/client/auth/ui/account-settings/change-password-panel/change-password-panel.tsx","../../common/resources/client/ui/forms/combobox/combobox-end-adornment.tsx","../../common/resources/client/ui/forms/combobox/combobox.tsx","../../common/resources/client/ui/forms/select/select.tsx","../../common/resources/client/auth/ui/account-settings/timezone-select.tsx","../../common/resources/client/auth/ui/account-settings/localization-panel.tsx","../../common/resources/client/i18n/use-date-formatter.ts","../../common/resources/client/i18n/formatted-date.tsx","../../common/resources/client/ui/overlays/dialog/confirmation-dialog.tsx","../../common/resources/client/auth/ui/account-settings/access-token-panel/delete-access-token.ts","../../common/resources/client/auth/ui/account-settings/access-token-panel/create-new-token.ts","../../common/resources/client/auth/ui/account-settings/access-token-panel/create-new-token-dialog.tsx","../../common/resources/client/auth/ui/account-settings/access-token-panel/secure-files.svg","../../common/resources/client/auth/ui/account-settings/access-token-panel/access-token-panel.tsx","../../common/resources/client/auth/ui/account-settings/danger-zone-panel/delete-account.ts","../../common/resources/client/auth/ui/account-settings/danger-zone-panel/danger-zone-panel.tsx","../../common/resources/client/auth/ui/two-factor/requests/use-enable-two-factor.ts","../../common/resources/client/auth/ui/two-factor/stepper/two-factor-stepper-layout.tsx","../../common/resources/client/auth/ui/confirm-password/requests/use-password-confirmation-status.ts","../../common/resources/client/auth/ui/confirm-password/requests/use-confirm-password.ts","../../common/resources/client/auth/ui/confirm-password/confirm-password-dialog.tsx","../../common/resources/client/auth/ui/confirm-password/use-password-confirmed-action.ts","../../common/resources/client/auth/ui/two-factor/stepper/two-factor-disabled-step.tsx","../../common/resources/client/auth/ui/two-factor/requests/use-two-factor-qr-code.ts","../../common/resources/client/auth/ui/two-factor/requests/use-confirm-two-factor.ts","../../common/resources/client/ui/skeleton/skeleton.tsx","../../common/resources/client/auth/ui/two-factor/requests/use-disable-two-factor.ts","../../common/resources/client/auth/ui/two-factor/stepper/two-factor-confirmation-step.tsx","../../common/resources/client/auth/ui/two-factor/requests/use-regenerate-two-factor-codes.ts","../../common/resources/client/auth/ui/two-factor/stepper/two-factor-enabled-step.tsx","../../common/resources/client/auth/ui/two-factor/stepper/two-factor-auth-stepper.tsx","../../common/resources/client/auth/ui/account-settings/sessions-panel/requests/use-user-sessions.ts","../../common/resources/client/icons/material/Computer.tsx","../../common/resources/client/icons/material/Smartphone.tsx","../../common/resources/client/icons/material/Tablet.tsx","../../common/resources/client/auth/ui/account-settings/sessions-panel/requests/use-logout-other-sessions.ts","../../common/resources/client/auth/ui/account-settings/sessions-panel/sessions-panel.tsx","../../common/resources/client/auth/ui/account-settings/account-settings-page.tsx","../../common/resources/client/auth/requests/send-reset-password-email.ts","../../common/resources/client/auth/ui/forgot-password-page.tsx","../../common/resources/client/auth/requests/reset-password.ts","../../common/resources/client/auth/ui/reset-password-page.tsx","../../common/resources/client/auth/auth-routes.tsx","../../common/resources/client/billing/pricing-table/use-products.ts","../../common/resources/client/icons/material/Forum.tsx","../../common/resources/client/ui/forms/radio-group/radio.tsx","../../common/resources/client/ui/forms/radio-group/radio-group.tsx","../../common/resources/client/billing/pricing-table/find-best-price.ts","../../common/resources/client/billing/pricing-table/upsell-label.tsx","../../common/resources/client/billing/pricing-table/billing-cycle-radio.tsx","../../common/resources/client/ui/forms/input-field/chip-field/cancel-filled-icon.tsx","../../common/resources/client/icons/material/Warning.tsx","../../common/resources/client/ui/tooltip/tooltip.tsx","../../common/resources/client/ui/forms/input-field/chip-field/chip.tsx","../../common/resources/client/i18n/formatted-currency.tsx","../../common/resources/client/i18n/formatted-price.tsx","../../common/resources/client/billing/pricing-table/product-feature-list.tsx","../../common/resources/client/billing/pricing-table/pricing-table.tsx","../../common/resources/client/billing/pricing-table/pricing-page.tsx","../../common/resources/client/billing/billing-routes.tsx","../../common/resources/client/notifications/notifications-page.tsx","../../common/resources/client/notifications/subscriptions/requests/notification-subscriptions.ts","../../common/resources/client/notifications/subscriptions/requests/update-notification-settings.ts","../../common/resources/client/notifications/subscriptions/notification-settings-page.tsx","../../common/resources/client/notifications/notification-routes.tsx","../../common/resources/client/contact/use-submit-contact-form.ts","../../common/resources/client/contact/contact-us-page.tsx","../../resources/client/app-routes.tsx","../../resources/client/server-entry.tsx"],"sourcesContent":["// store this in a separate file, to avoid importing query client and axios in pixie\n\nlet activeWorkspaceId = 0;\n\n// for access outside react\nexport function getActiveWorkspaceId() {\n return activeWorkspaceId;\n}\n\nexport function setActiveWorkspaceId(id: number) {\n activeWorkspaceId = id;\n}\n","export function isAbsoluteUrl(url?: string): boolean {\n if (!url) return false;\n return /^[a-zA-Z][a-zA-Z\\d+\\-.]*?:/.test(url);\n}\n","import axios from 'axios';\n\nexport function errorStatusIs(err: unknown, status: number): boolean {\n return axios.isAxiosError(err) && err.response?.status == status;\n}\n","import {QueryClient} from '@tanstack/react-query';\nimport axios, {AxiosRequestConfig} from 'axios';\nimport {getActiveWorkspaceId} from '../workspace/active-workspace-id';\nimport {isAbsoluteUrl} from '../utils/urls/is-absolute-url';\nimport {errorStatusIs} from '@common/utils/http/error-status-is';\n\nexport const queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 30000,\n retry: (failureCount, err) => {\n return (\n !errorStatusIs(err, 401) &&\n !errorStatusIs(err, 403) &&\n !errorStatusIs(err, 404) &&\n failureCount < 2\n );\n },\n },\n },\n});\n\nexport const apiClient = axios.create();\napiClient.defaults.withCredentials = true;\napiClient.defaults.responseType = 'json';\n// @ts-ignore\napiClient.defaults.headers = {\n common: {\n Accept: 'application/json',\n },\n};\n\n// @ts-ignore\napiClient.interceptors.request.use((config: AxiosRequestConfig) => {\n if (\n !config.url?.startsWith('auth') &&\n !config.url?.startsWith('secure') &&\n !isAbsoluteUrl(config?.url)\n ) {\n config.url = `api/v1/${config.url}`;\n }\n\n const method = config.method?.toUpperCase();\n\n // transform array query params in GET request to comma separated string\n if (method === 'GET' && Array.isArray(config.params?.with)) {\n config.params.with = config.params.with.join(',');\n }\n if (method === 'GET' && Array.isArray(config.params?.load)) {\n config.params.load = config.params.load.join(',');\n }\n if (method === 'GET' && Array.isArray(config.params?.loadCount)) {\n config.params.loadCount = config.params.loadCount.join(',');\n }\n\n // add workspace query parameter\n const workspaceId = getActiveWorkspaceId();\n if (workspaceId) {\n const method = config.method?.toLowerCase();\n if (['get', 'post', 'put'].includes(method!)) {\n config.params = {...config.params, workspaceId};\n }\n }\n\n // override PUT, DELETE, PATCH methods, they might not be supported on the backend\n if (method === 'PUT' || method === 'DELETE' || method === 'PATCH') {\n config.headers = {\n ...config.headers,\n 'X-HTTP-Method-Override': method,\n };\n config.method = 'POST';\n config.params = {\n ...config.params,\n _method: method,\n };\n }\n\n if (import.meta.env.SSR) {\n config.headers = {\n ...config.headers,\n referer: 'http://localhost',\n };\n }\n\n return config;\n});\n","import {apiClient, queryClient} from '../../http/query-client';\nimport {BootstrapData} from './bootstrap-data';\nimport {keepPreviousData, useQuery} from '@tanstack/react-query';\n\nconst queryKey = ['bootstrapData'];\n\nexport function getBootstrapData(): BootstrapData {\n return queryClient.getQueryData(queryKey)!;\n}\n\nexport function invalidateBootstrapData() {\n queryClient.invalidateQueries({queryKey});\n}\n\nexport function setBootstrapData(data: string | BootstrapData) {\n queryClient.setQueryData(\n queryKey,\n typeof data === 'string' ? decodeBootstrapData(data) : data,\n );\n}\n\nexport function mergeBootstrapData(partialData: Partial) {\n setBootstrapData({\n ...getBootstrapData(),\n ...partialData,\n });\n}\n\n// set bootstrap data that was provided with initial request from backend\nconst initialBootstrapData = (\n typeof window !== 'undefined' && window.bootstrapData\n ? decodeBootstrapData(window.bootstrapData)\n : undefined\n) as BootstrapData;\n\n// make sure initial data is available right away when accessing it via \"getBootstrapData()\"\nqueryClient.setQueryData(queryKey, initialBootstrapData);\n\nexport function useBackendBootstrapData() {\n return useQuery({\n queryKey: queryKey,\n queryFn: () => fetchBootstrapData(),\n staleTime: Infinity,\n placeholderData: keepPreviousData,\n initialData: initialBootstrapData,\n });\n}\n\nconst fetchBootstrapData = async (): Promise => {\n return apiClient\n .get('http://bedesk.test/api/v1/bootstrap-data')\n .then(response => {\n return decodeBootstrapData(response.data.data);\n });\n};\n\nfunction decodeBootstrapData(data: string | BootstrapData): BootstrapData {\n return typeof data === 'string' ? JSON.parse(data) : data;\n}\n","import React, {ComponentType} from 'react';\nimport type {NotificationListItemProps} from '../../notifications/notification-list';\nimport {MessageDescriptor} from '../../i18n/message-descriptor';\nimport {User} from '@common/auth/user';\nimport {SvgIconProps} from '@common/icons/svg-icon';\nimport {AccountSettingsId} from '@common/auth/ui/account-settings/account-settings-sidenav';\n\nexport interface AdConfig {\n slot: string;\n description: MessageDescriptor;\n image: string;\n}\n\nexport interface TagType {\n name: string;\n system?: boolean;\n}\n\nexport interface CustomPageType {\n type: string;\n label: MessageDescriptor;\n}\n\nexport interface HomepageOption {\n label: MessageDescriptor;\n value: string;\n}\n\nexport interface SiteConfigContextValue {\n auth: {\n redirectUri: string;\n adminRedirectUri: string;\n getUserProfileLink?: (user: User) => string;\n registerFields?: ComponentType;\n accountSettingsPanels?: {\n icon: ComponentType;\n label: MessageDescriptor;\n id: AccountSettingsId;\n component: ComponentType<{user: User}>;\n }[];\n };\n notifications: {\n renderMap?: Record>;\n };\n tags: {\n types: TagType[];\n };\n customPages: {\n types: CustomPageType[];\n };\n settings?: {\n showIncomingMailMethod?: boolean;\n showRecaptchaLinkSwitch?: boolean;\n };\n admin: {\n ads: AdConfig[];\n };\n demo: {\n loginPageDefaults: 'singleAccount' | 'randomAccount';\n };\n homepage: {\n options: HomepageOption[];\n };\n}\n\nexport const SiteConfigContext = React.createContext(\n null!,\n);\n","import {MessageDescriptor} from './message-descriptor';\n\ninterface MessageProps extends Omit {}\nexport function message(msg: string, props?: MessageProps): MessageDescriptor {\n return {...props, message: msg};\n}\n","export default \"__VITE_ASSET__dc8da3aa__\"","export default \"__VITE_ASSET__77a4ae48__\"","export default \"__VITE_ASSET__2759f39a__\"","import React, {forwardRef} from 'react';\nimport clsx from 'clsx';\n\nexport type IconSize = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | string;\n\nexport interface SvgIconProps extends React.SVGAttributes {\n children?: React.ReactNode;\n size?: IconSize;\n color?: string;\n title?: string;\n}\n\nexport const SvgIcon = forwardRef(\n (props, ref) => {\n const {\n attr,\n size,\n title,\n className,\n color,\n style,\n children,\n viewBox,\n width,\n height,\n ...svgProps\n } = props;\n\n return (\n \n {title && {title}}\n {children}\n \n );\n }\n);\n\nfunction getSizeClassName(size?: IconSize) {\n switch (size) {\n case '2xs':\n return 'icon-2xs';\n case 'xs':\n return 'icon-xs';\n case 'sm':\n return 'icon-sm';\n case 'md':\n return 'icon-md';\n case 'lg':\n return 'icon-lg';\n case 'xl':\n return 'icon-xl';\n default:\n return size;\n }\n}\n","import React, {ComponentType, ReactElement, RefObject} from 'react';\nimport {SvgIcon, SvgIconProps} from './svg-icon';\n\nexport function createSvgIcon(\n path: ReactElement | ReactElement[],\n displayName: string = '',\n viewBox?: string\n): ComponentType {\n const Component = (props: SvgIconProps, ref: RefObject) => (\n \n {path}\n \n );\n\n if (process.env.NODE_ENV !== 'production') {\n // Need to set `displayName` on the inner component for React.memo.\n // React prior to 16.14 ignores `displayName` on the wrapper.\n Component.displayName = `${displayName}Icon`;\n }\n\n return React.memo(React.forwardRef(Component as any));\n}\n\nexport interface IconTree {\n tag: string;\n attr?: {[key: string]: string};\n // Can't use \"IconTree\", otherwise there's circular reference error in hook form\n child?: {tag: string; attr?: {[key: string]: string}}[];\n}\nexport function createSvgIconFromTree(\n data: IconTree[],\n displayName: string = ''\n) {\n const path = treeToElement(data);\n return createSvgIcon(path!, displayName);\n}\n\nfunction treeToElement(\n tree?: IconTree[]\n): React.ReactElement<{}>[] | undefined {\n return (\n tree?.map &&\n tree.map((node, i) => {\n return React.createElement(\n node.tag,\n {key: i, ...node.attr},\n treeToElement(node.child)\n );\n })\n );\n}\n\nexport function elementToTree(el: HTMLElement | SVGElement): IconTree {\n const attributes: IconTree['attr'] = {};\n const tree: IconTree = {tag: el.tagName, attr: attributes};\n Array.from(el.attributes).forEach(attribute => {\n attributes[attribute.name] = attribute.value;\n });\n if (el.children.length) {\n tree.child = Array.from(el.children).map(child =>\n elementToTree(child as HTMLElement)\n );\n }\n return tree;\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const GroupAddIcon = createSvgIcon(\n \n, 'GroupAddOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const PeopleIcon = createSvgIcon(\n \n, 'PeopleOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const FileDownloadDoneIcon = createSvgIcon(\n \n, 'FileDownloadDoneOutlined');\n","import {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';\nimport {isAbsoluteUrl} from '@common/utils/urls/is-absolute-url';\n\nexport function getAssetUrl(url: string) {\n if (isAbsoluteUrl(url)) {\n return url;\n }\n const assetUrl =\n getBootstrapData().settings.asset_url ||\n getBootstrapData().settings.base_url;\n\n //remove leading slash\n url = url.replace(/^\\/+/g, '');\n\n if (url.startsWith('assets/')) {\n return `${assetUrl}/build/${url}`;\n }\n\n return `${assetUrl}/${url}`;\n}\n","import axios from 'axios';\nimport {useQuery} from '@tanstack/react-query';\nimport {memo} from 'react';\nimport clsx from 'clsx';\nimport {getAssetUrl} from '@common/utils/urls/get-asset-url';\n\ntype DangerousHtml = {__html: string} | undefined;\n\ninterface Props {\n src: string;\n className?: string;\n height?: string | false;\n}\nexport const SvgImage = memo(({src, className, height = 'h-full'}: Props) => {\n const {data: svgString} = useSvgImageContent(src);\n // render container even if image is not loaded yet, so there's\n // no layout shift if height is provided via className\n return (\n \n );\n});\n\nfunction useSvgImageContent(src: string) {\n return useQuery({\n queryKey: ['svgImage', getAssetUrl(src)],\n queryFn: () => fetchSvgImageContent(src),\n refetchOnMount: false,\n refetchOnReconnect: false,\n refetchOnWindowFocus: false,\n staleTime: Infinity,\n enabled: !!src,\n });\n}\n\nfunction fetchSvgImageContent(src: string): Promise {\n return axios\n .get(src, {\n responseType: 'text',\n })\n .then(response => {\n return {__html: response.data};\n });\n}\n","import {ComponentType, HTMLAttributes, memo} from 'react';\nimport {SvgImage} from './svg-image/svg-image';\nimport {SvgIconProps} from '../../icons/svg-icon';\nimport {isAbsoluteUrl} from '@common/utils/urls/is-absolute-url';\n\ninterface Props extends HTMLAttributes {\n src: string | ComponentType;\n className?: string;\n}\nexport const MixedImage = memo(({src, className, ...domProps}: Props) => {\n let type: 'svg' | 'image' | 'icon' | null = null;\n\n if (!src) {\n type = null;\n } else if (typeof src === 'object') {\n type = 'icon';\n } else if (\n (src as string).endsWith('.svg') &&\n !isAbsoluteUrl(src as string)\n ) {\n type = 'svg';\n } else {\n type = 'image';\n }\n\n if (type === 'svg') {\n return (\n \n );\n }\n\n if (type === 'image') {\n return (\n \"\"\n );\n }\n\n if (type === 'icon') {\n const Icon = src;\n return (\n )}\n className={className}\n />\n );\n }\n\n return null;\n});\n","import {ButtonVariant} from './get-shared-button-style';\n\nexport type ButtonSize = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | null;\n\ninterface Props {\n padding?: string;\n equalWidth?: boolean;\n variant?: ButtonVariant;\n}\n\nexport function getButtonSizeStyle(\n size?: ButtonSize,\n {padding, equalWidth, variant}: Props = {}\n): string {\n switch (size) {\n case '2xs':\n if (variant === 'link') return 'text-xs';\n return `text-xs h-24 ${equalWidth ? 'w-24' : padding || 'px-10'}`;\n case 'xs':\n if (variant === 'link') return 'text-xs';\n return `text-xs h-30 ${equalWidth ? 'w-30' : padding || 'px-14'}`;\n case 'sm':\n if (variant === 'link') return 'text-sm';\n return `text-sm h-36 ${equalWidth ? 'w-36' : padding || 'px-18'}`;\n case 'md':\n if (variant === 'link') return 'text-base';\n return `text-base h-42 ${equalWidth ? 'w-42' : padding || 'px-22'}`;\n case 'lg':\n if (variant === 'link') return 'text-lg';\n return `text-base h-50 ${equalWidth ? 'w-50' : padding || 'px-26'}`;\n case 'xl':\n if (variant === 'link') return 'text-xl';\n return `text-lg h-60 ${equalWidth ? 'w-60' : padding || 'px-32'}`;\n default:\n return size || '';\n }\n}\n","export type ButtonVariant =\n | 'text'\n | 'flat'\n | 'raised'\n | 'outline'\n | 'link'\n | null;\nexport type ButtonColor =\n | null\n | 'primary'\n | 'danger'\n | 'positive'\n | 'paper'\n | 'chip'\n | 'white';\n\ninterface SharedButtonStyleProps {\n variant?: ButtonVariant;\n color?: ButtonColor;\n border?: string;\n shadow?: string;\n whitespace?: string;\n display?: string;\n}\nexport function getSharedButtonStyle(\n props: SharedButtonStyleProps,\n): (string | boolean | null | undefined)[] {\n const {\n variant,\n shadow,\n whitespace = 'whitespace-nowrap',\n display = 'inline-flex',\n } = props;\n const variantProps = {...props, border: props.border || 'border'};\n let style: string[] = [];\n if (variant === 'outline') {\n style = outline(variantProps);\n } else if (variant === 'text') {\n style = text(variantProps);\n } else if (variant === 'flat' || variant === 'raised') {\n style = contained(variantProps);\n } else if (variant === 'link') {\n style = link(variantProps);\n }\n\n return [\n ...style,\n shadow || (variant === 'raised' && 'shadow-md'),\n whitespace,\n display,\n variant &&\n 'align-middle flex-shrink-0 items-center transition-button duration-200',\n 'select-none appearance-none no-underline outline-none disabled:pointer-events-none disabled:cursor-default',\n ];\n}\n\nfunction outline({color, border}: SharedButtonStyleProps) {\n const disabled =\n 'disabled:text-disabled disabled:bg-transparent disabled:border-disabled-bg';\n switch (color) {\n case 'primary':\n return [\n `text-primary bg-transparent ${border} border-primary/50`,\n 'hover:bg-primary/hover hover:border-primary',\n disabled,\n ];\n case 'danger':\n return [\n `text-danger bg-transparent ${border} border-danger/50`,\n 'hover:bg-danger/4 hover:border-danger',\n disabled,\n ];\n case 'positive':\n return [\n `text-positive bg-transparent ${border} border-positive/50`,\n 'hover:bg-positive/4 hover:border-positive',\n disabled,\n ];\n case 'paper':\n return [`text bg-paper ${border}`, 'hover:bg-hover', disabled];\n case 'white':\n return [\n 'text-white bg-transparent border border-white',\n 'hover:bg-white/20',\n 'disabled:text-white/70 disabled:border-white/70 disabled:bg-transparent',\n ];\n default:\n return [`bg-transparent ${border}`, 'hover:bg-hover', disabled];\n }\n}\n\nfunction text({color}: SharedButtonStyleProps) {\n const disabled = 'disabled:text-disabled disabled:bg-transparent';\n switch (color) {\n case 'primary':\n return [\n 'text-primary bg-transparent border-transparent',\n 'hover:bg-primary/4',\n disabled,\n ];\n case 'danger':\n return [\n 'text-danger bg-transparent border-transparent',\n 'hover:bg-danger/4',\n disabled,\n ];\n case 'positive':\n return [\n 'text-positive bg-transparent border-transparent',\n 'hover:bg-positive/4',\n disabled,\n ];\n case 'white':\n return [\n 'text-white bg-transparent border-transparent',\n 'hover:bg-white/20',\n 'disabled:text-white/70 disabled:bg-transparent',\n ];\n default:\n return ['bg-transparent border-transparent', 'hover:bg-hover', disabled];\n }\n}\n\nfunction link({color}: SharedButtonStyleProps) {\n switch (color) {\n case 'primary':\n return ['text-primary', 'hover:underline', 'disabled:text-disabled'];\n case 'danger':\n return ['text-danger', 'hover:underline', 'disabled:text-disabled'];\n default:\n return ['text-main', 'hover:underline', 'disabled:text-disabled'];\n }\n}\n\nfunction contained({color, border}: SharedButtonStyleProps) {\n const disabled =\n 'disabled:text-disabled disabled:bg-disabled disabled:border-transparent disabled:shadow-none';\n switch (color) {\n case 'primary':\n return [\n `text-on-primary bg-primary ${border} border-primary`,\n 'hover:bg-primary-dark hover:border-primary-dark',\n disabled,\n ];\n case 'danger':\n return [\n `text-white bg-danger ${border} border-danger`,\n 'hover:bg-danger/90 hover:border-danger/90',\n disabled,\n ];\n case 'chip':\n return [\n `text-main bg-chip ${border} border-chip`,\n 'hover:bg-chip/90 hover:border-chip/90',\n disabled,\n ];\n case 'paper':\n return [\n `text-main bg-paper ${border} border-paper`,\n 'hover:bg-paper/90 hover:border-paper/90',\n disabled,\n ];\n case 'white':\n return [\n `text-black bg-white ${border} border-white`,\n 'hover:bg-white',\n disabled,\n ];\n default:\n return [`bg ${border} border-background`, 'hover:bg-hover', disabled];\n }\n}\n","import {EventHandler, SyntheticEvent} from 'react';\n\nexport function createEventHandler(handler?: EventHandler) {\n if (!handler) return handler;\n\n return (e: SyntheticEvent) => {\n // ignore events bubbling up from portals\n if (e.currentTarget.contains(e.target as HTMLElement)) {\n handler(e);\n }\n };\n}\n","import React, {\n ComponentPropsWithRef,\n forwardRef,\n JSXElementConstructor,\n} from 'react';\nimport clsx from 'clsx';\nimport {RelativeRoutingType, To} from 'react-router-dom';\nimport {\n ButtonColor,\n ButtonVariant,\n getSharedButtonStyle,\n} from './get-shared-button-style';\nimport {createEventHandler} from '../../utils/dom/create-event-handler';\n\nexport interface ButtonBaseProps\n extends Omit, 'color'> {\n color?: ButtonColor;\n variant?: ButtonVariant;\n value?: any;\n justify?: string;\n display?: string;\n radius?: string;\n shadow?: string;\n border?: string;\n whitespace?: string;\n form?: string;\n to?: To;\n relative?: RelativeRoutingType;\n href?: string;\n target?: '_blank';\n rel?: string;\n replace?: boolean;\n end?: boolean;\n elementType?: 'button' | 'a' | JSXElementConstructor;\n download?: boolean;\n}\n\nexport const ButtonBase = forwardRef<\n HTMLButtonElement | HTMLLinkElement,\n ButtonBaseProps\n>((props, ref) => {\n const {\n children,\n color = null,\n variant,\n radius,\n shadow,\n whitespace,\n justify = 'justify-center',\n className,\n href,\n form,\n border,\n elementType,\n to,\n relative,\n replace,\n end,\n display,\n type = 'button',\n onClick,\n onPointerDown,\n onPointerUp,\n onKeyDown,\n ...domProps\n } = props;\n const Element = elementType || (href ? 'a' : 'button');\n const isLink = Element === 'a';\n\n return (\n \n {children}\n \n );\n});\n","import React, {ReactElement} from 'react';\nimport clsx from 'clsx';\nimport {ButtonSize, getButtonSizeStyle} from './button-size';\nimport {ButtonBase, ButtonBaseProps} from './button-base';\nimport {IconSize} from '../../icons/svg-icon';\n\nexport interface ButtonProps extends ButtonBaseProps {\n size?: ButtonSize;\n sizeClassName?: string;\n equalWidth?: boolean;\n startIcon?: ReactElement | null | false;\n endIcon?: ReactElement | null | false;\n}\nexport const Button = React.forwardRef(\n (\n {\n children,\n startIcon,\n endIcon,\n size = 'sm',\n sizeClassName,\n className,\n equalWidth = false,\n radius = 'rounded',\n variant = 'text',\n ...other\n },\n ref\n ) => {\n const mergedClassName = clsx(\n 'font-semibold',\n sizeClassName || getButtonSizeStyle(size, {equalWidth, variant}),\n className\n );\n return (\n \n {startIcon && (\n \n )}\n {children}\n {endIcon && }\n \n );\n }\n);\n\ntype InlineIconProps = {\n icon: ReactElement;\n position: 'start' | 'end';\n size?: IconSize | null;\n};\nfunction InlineIcon({icon, position, size}: InlineIconProps): ReactElement {\n const className = clsx(\n 'm-auto',\n {\n '-ml-4 mr-8': position === 'start',\n '-mr-4 ml-8': position === 'end',\n },\n icon.props.className\n );\n return React.cloneElement(icon, {className, size});\n}\n","export function shallowEqual<\n T extends Record = Record\n>(objA?: T, objB?: T) {\n if (objA === objB) {\n return true;\n }\n\n if (!objA || !objB) {\n return false;\n }\n\n const aKeys = Object.keys(objA);\n const bKeys = Object.keys(objB);\n const len = aKeys.length;\n\n if (bKeys.length !== len) {\n return false;\n }\n\n for (let i = 0; i < len; i++) {\n const key = aKeys[i];\n\n if (\n objA[key] !== objB[key] ||\n !Object.prototype.hasOwnProperty.call(objB, key)\n ) {\n return false;\n }\n }\n\n return true;\n}\n","import {BootstrapData} from './bootstrap-data';\nimport {createContext, useContext} from 'react';\n\nexport interface BoostrapDataContextValue {\n data: T;\n setBootstrapData: (data: string | T) => void;\n mergeBootstrapData: (data: Partial) => void;\n invalidateBootstrapData: () => void;\n}\n\nexport const BoostrapDataContext = createContext(\n null!\n);\n\nexport function useBootstrapData() {\n return useContext(BoostrapDataContext);\n}\n","import {useBootstrapData} from '../core/bootstrap-data/bootstrap-data-context';\n\nexport function useSelectedLocale() {\n const {\n data: {i18n},\n } = useBootstrapData();\n return {\n locale: i18n,\n localeCode: i18n?.language || 'en',\n lines: i18n?.lines,\n };\n}\n","import {useContext, useMemo} from 'react';\nimport {BoostrapDataContext} from '../core/bootstrap-data/bootstrap-data-context';\nimport {getLocalTimeZone} from '@internationalized/date';\n\nexport function useUserTimezone(): string {\n const {\n data: {user, settings},\n } = useContext(BoostrapDataContext);\n const defaultTimezone = settings.dates.default_timezone;\n const preferredTimezone = user?.timezone || defaultTimezone || 'auto';\n\n return useMemo(() => {\n return !preferredTimezone || preferredTimezone === 'auto'\n ? getLocalTimeZone()\n : preferredTimezone;\n }, [preferredTimezone]);\n}\n","import memoize from 'nano-memoize';\nimport {MessageDescriptor} from './message-descriptor';\n\n// this will get memoized by enclosing function ( or useTrans)\nexport function handlePluralMessage(\n localeCode: string,\n {message, values}: MessageDescriptor\n): string {\n // find plural config e.g. [one 1 item|other :count items]\n const match = message.match(/\\[(.+?)]/);\n const count = values?.count;\n if (match && match[1] && !Number.isNaN(count)) {\n // get config without brackets and split by pipe e.g. [one 1 item, other :count items]\n const [pluralPlaceholder, pluralConfig] = match;\n const choices = pluralConfig.split('|');\n if (!choices.length) return message;\n\n // use Intl.PluralRules to determine which choice to use, based on special \"count\" value\n const rules = getRules(localeCode);\n const choiceName = rules.select(count as number);\n\n // find the correct choice from config, or use first one\n let choiceConfig = choices.find(c => {\n return c.startsWith(choiceName);\n });\n if (!choiceConfig) {\n choiceConfig = choices[0];\n }\n\n // get rid of plural prefix e.g. one 1 item => 1 item\n const choice = choiceConfig.substring(choiceConfig.indexOf(' ') + 1);\n\n return message.replace(pluralPlaceholder, choice);\n }\n return message;\n}\n\nconst getRules = memoize((localeCode: string) => {\n return new Intl.PluralRules(localeCode);\n});\n","import {cloneElement, Fragment, isValidElement, memo} from 'react';\nimport {shallowEqual} from '../utils/shallow-equal';\nimport {useSelectedLocale} from './selected-locale';\nimport {handlePluralMessage} from './handle-plural-message';\nimport {MessageDescriptor} from './message-descriptor';\n\nexport const Trans = memo((props: MessageDescriptor) => {\n const {message: initialMessage, values} = props;\n const {lines, localeCode} = useSelectedLocale();\n let translatedMessage: string | undefined;\n\n if (Object.hasOwn(lines || {}, initialMessage)) {\n translatedMessage = lines?.[initialMessage];\n } else if (Object.hasOwn(lines || {}, initialMessage?.toLowerCase())) {\n translatedMessage = lines?.[initialMessage.toLowerCase()];\n } else {\n translatedMessage = initialMessage;\n }\n\n if (!values || !translatedMessage) {\n return {translatedMessage};\n }\n\n translatedMessage = handlePluralMessage(localeCode, {\n message: translatedMessage,\n values,\n });\n\n // placeholders that need to be replaced with react element, eg. \n const nodePlaceholders: string[] = [];\n // placeholders that need to be replaced with render fn, eg. link text\n const tagNames: string[] = [];\n\n Object.entries(values).forEach(([key, value]) => {\n // value is react render function\n if (typeof value === 'function') {\n tagNames.push(key);\n // value is react element\n } else if (isValidElement(value)) {\n nodePlaceholders.push(key);\n // value is primitive, can do simple string replace\n } else if (value != undefined) {\n translatedMessage = translatedMessage?.replace(`:${key}`, `${value}`);\n }\n });\n\n // if we need to replace placeholder with react element or render fn, we will need to split the\n // string by these placeholders and replace static string values with matching react element value\n if (tagNames.length || nodePlaceholders.length) {\n // we'll build simple OR regex to split the string eg. (<[ab]>content)|({(?:icon|link)})\n const regexArray: string[] = [];\n if (tagNames.length) {\n const tagNameMatchers = tagNames.join('');\n regexArray.push(`(<[${tagNameMatchers}]>.+?<\\\\/[${tagNameMatchers}]>)`);\n }\n if (nodePlaceholders.length) {\n const nodePlaceholderMatchers = nodePlaceholders.join('|');\n regexArray.push(`(\\:(?:${nodePlaceholderMatchers}))`);\n }\n\n const regex = new RegExp(regexArray.join('|'), 'gm');\n const parts = translatedMessage.split(regex);\n\n // get rid of any empty strings or undefined from split by regex\n const compiledMessage = parts.filter(Boolean).map((part, i) => {\n // it's a tag name placeholder, eg. content\n if (part.startsWith('<') && part.endsWith('>')) {\n // grab tag content\n const matches = part.match(/<([a-z]+)>(.+?)<\\/([a-z]+)>/);\n if (matches) {\n const [, tagName, content] = matches;\n const renderFn = values?.[tagName];\n if (typeof renderFn === 'function') {\n // pass it to render fn from values\n const node = renderFn(content);\n // add a key to avoid react errors\n return cloneElement(node, {key: i});\n }\n }\n }\n\n // it's a regular placeholder with react element value, eg. {icon}\n if (part.startsWith(':')) {\n const key = part.replace(':', '');\n const node = values?.[key];\n if (isValidElement(node)) {\n return cloneElement(node, {key: i});\n }\n }\n\n // it's a regular string\n return part;\n });\n return {compiledMessage};\n }\n\n return {translatedMessage};\n}, areEqual);\n\nexport function areEqual(\n prevProps: T,\n nextProps: T,\n): boolean {\n const {values, ...otherProps} = prevProps;\n const {values: nextValues, ...nextOtherProps} = nextProps;\n return (\n shallowEqual(nextValues, values) &&\n shallowEqual(otherProps as any, nextOtherProps)\n );\n}\n","import {DateValue, parseAbsoluteToLocal} from '@internationalized/date';\nimport {Fragment, memo, useMemo} from 'react';\nimport {shallowEqual} from '@common/utils/shallow-equal';\nimport {useSelectedLocale} from '@common/i18n/selected-locale';\nimport {useUserTimezone} from '@common/i18n/use-user-timezone';\nimport {Trans} from '@common/i18n/trans';\n\nconst DIVISIONS: {amount: number; name: Intl.RelativeTimeFormatUnit}[] = [\n {amount: 60, name: 'seconds'},\n {amount: 60, name: 'minutes'},\n {amount: 24, name: 'hours'},\n {amount: 7, name: 'days'},\n {amount: 4.34524, name: 'weeks'},\n {amount: 12, name: 'months'},\n {amount: Number.POSITIVE_INFINITY, name: 'years'},\n];\n\ninterface FormattedDateProps {\n date?: string | DateValue | Date;\n style?: Intl.RelativeTimeFormatStyle;\n}\nexport const FormattedRelativeTime = memo(\n ({date, style}: FormattedDateProps) => {\n const {localeCode} = useSelectedLocale();\n const timezone = useUserTimezone();\n\n const formatter = useMemo(\n () =>\n new Intl.RelativeTimeFormat(localeCode, {\n numeric: 'auto',\n style,\n }),\n [localeCode, style]\n );\n\n if (!date) {\n return null;\n }\n\n // make sure date with invalid format does not blow up the app\n try {\n if (typeof date === 'string') {\n date = parseAbsoluteToLocal(date).toDate();\n } else if ('toDate' in date) {\n date = date.toDate(timezone);\n }\n } catch (e) {\n return null;\n }\n\n let duration = (date.getTime() - Date.now()) / 1000;\n\n for (let i = 0; i <= DIVISIONS.length; i++) {\n const division = DIVISIONS[i];\n if (Math.abs(duration) < division.amount) {\n if (division.name === 'seconds') {\n return ;\n }\n return (\n \n {formatter.format(Math.round(duration), division.name)}\n \n );\n }\n duration /= division.amount;\n }\n\n return {formatter.format(Math.round(duration), 'day')};\n },\n shallowEqual\n);\n","import {MixedImage} from '../ui/images/mixed-image';\nimport clsx from 'clsx';\nimport React, {JSXElementConstructor} from 'react';\nimport {\n DatabaseNotification,\n DatabaseNotificationLine,\n} from './database-notification';\nimport {FormattedRelativeTime} from '@common/i18n/formatted-relative-time';\n\ninterface LineProps {\n notification: DatabaseNotification;\n line: DatabaseNotificationLine;\n index: number;\n iconRenderer?: JSXElementConstructor<{icon: string}>;\n}\n\nexport function Line({notification, line, index, iconRenderer}: LineProps) {\n const isPrimary = line.type === 'primary' || index === 0;\n const Icon = iconRenderer || DefaultIconRenderer;\n const Element = line.action ? 'a' : 'div';\n\n return (\n <>\n \n {line.icon && }\n \n \n {index === 0 && (\n \n )}\n \n );\n}\n\ninterface DefaultIconRendererProps {\n icon: string;\n}\nfunction DefaultIconRenderer({icon}: DefaultIconRendererProps) {\n return ;\n}\n","import {useQuery} from '@tanstack/react-query';\nimport {PaginatedBackendResponse} from '@common/http/backend-response/pagination-response';\nimport {DatabaseNotification} from '@common/notifications/database-notification';\nimport {apiClient} from '@common/http/query-client';\n\nconst Endpoint = 'notifications';\n\nexport interface FetchUserNotificationsResponse\n extends PaginatedBackendResponse {\n //\n}\n\ninterface Payload {\n perPage?: number;\n}\n\nexport function useUserNotifications(payload?: Payload) {\n return useQuery({\n queryKey: useUserNotifications.key,\n queryFn: () => fetchUserNotifications(payload),\n });\n}\n\nfunction fetchUserNotifications(\n payload?: Payload,\n): Promise {\n return apiClient\n .get(Endpoint, {params: payload})\n .then(response => response.data);\n}\n\nuseUserNotifications.key = [Endpoint];\n","export class ToastTimer {\n private timerId?: ReturnType;\n private createdAt: number = 0;\n\n constructor(private callback: () => void, private remaining: number) {\n this.resume();\n }\n\n pause() {\n clearTimeout(this.timerId);\n this.remaining -= Date.now() - this.createdAt;\n }\n\n resume() {\n this.createdAt = Date.now();\n if (this.timerId) {\n clearTimeout(this.timerId);\n }\n this.timerId = setTimeout(this.callback, this.remaining);\n }\n\n clear() {\n clearTimeout(this.timerId);\n }\n}\n","import {create} from 'zustand';\nimport {immer} from 'zustand/middleware/immer';\nimport {MessageDescriptor} from '../../i18n/message-descriptor';\nimport {nanoid} from 'nanoid';\nimport {ToastTimer} from './toast-timer';\n\ntype ToastType = 'danger' | 'default' | 'positive' | 'loading' | null;\ntype ToastPosition = 'bottom-center' | 'bottom-right';\n\ninterface ToastAction {\n label: string | MessageDescriptor;\n action: string;\n}\n\nexport interface ToastOptions {\n type?: ToastType;\n action?: ToastAction;\n id?: string | number;\n duration?: number;\n position?: 'bottom-center' | 'bottom-right';\n disableExitAnimation?: boolean;\n disableEnterAnimation?: boolean;\n}\n\ninterface Toast {\n timer?: ToastTimer | null;\n message: string | MessageDescriptor;\n type: ToastType;\n id: string | number;\n duration: number;\n action?: ToastAction;\n position: ToastPosition;\n disableExitAnimation?: boolean;\n disableEnterAnimation?: boolean;\n}\n\ninterface ToastStore {\n toasts: Toast[];\n add: (message: Toast['message'], opts?: ToastOptions) => void;\n remove: (toastId: string | number) => void;\n}\n\nconst maximumVisible = 1;\n\nfunction getDefaultDuration(type: ToastType) {\n switch (type) {\n case 'danger':\n return 8000;\n case 'loading':\n return 0;\n default:\n return 3000;\n }\n}\n\nexport const useToastStore = create()(\n immer((set, get) => ({\n toasts: [],\n add: (message, opts) => {\n const amountToRemove = get().toasts.length + 1 - maximumVisible;\n if (amountToRemove > 0) {\n set(state => {\n state.toasts.splice(0, amountToRemove);\n });\n }\n\n const toastId = opts?.id || nanoid(6);\n const toastType = opts?.type || 'positive';\n const duration = opts?.duration ?? getDefaultDuration(toastType);\n const toast: Toast = {\n timer:\n duration > 0\n ? new ToastTimer(() => get().remove(toastId), duration)\n : null,\n message,\n ...opts,\n id: toastId,\n type: toastType,\n position: opts?.position || 'bottom-center',\n duration,\n disableExitAnimation: opts?.disableExitAnimation,\n disableEnterAnimation: opts?.disableEnterAnimation,\n };\n\n const toastIndex = get().toasts.findIndex(t => t.id === toast.id);\n if (toastIndex > -1) {\n set(state => {\n state.toasts[toastIndex] = toast;\n });\n } else {\n set(state => {\n state.toasts.push(toast);\n });\n }\n },\n remove: toastId => {\n const newToasts = get().toasts.filter(toast => {\n if (toastId === toast.id) {\n toast.timer?.clear();\n return false;\n }\n return true;\n });\n set(state => {\n state.toasts = newToasts;\n });\n },\n }))\n);\n\nexport function toastState() {\n return useToastStore.getState();\n}\n","import {MessageDescriptor} from '../../i18n/message-descriptor';\nimport {ToastOptions, toastState} from './toast-store';\n\nexport function toast(\n message: MessageDescriptor | string,\n opts?: ToastOptions\n) {\n toastState().add(message, opts);\n}\n\ntoast.danger = (message: MessageDescriptor | string, opts?: ToastOptions) => {\n toastState().add(message, {...opts, type: 'danger'});\n};\n\ntoast.positive = (message: MessageDescriptor | string, opts?: ToastOptions) => {\n toastState().add(message, {...opts, type: 'positive'});\n};\n\ntoast.loading = (message: MessageDescriptor | string, opts?: ToastOptions) => {\n toastState().add(message, {...opts, type: 'loading'});\n};\n","import axios from 'axios';\nimport {BackendErrorResponse} from '../../errors/backend-error-response';\n\nexport function getAxiosErrorMessage(\n err: unknown,\n field?: string | null\n): string | undefined {\n if (axios.isAxiosError(err) && err.response) {\n const response = err.response.data as BackendErrorResponse;\n\n if (field != null) {\n const fieldMessage = response.errors?.[field];\n return Array.isArray(fieldMessage) ? fieldMessage[0] : fieldMessage;\n }\n\n return response?.message;\n }\n}\n","import {toast} from '../../ui/toast/toast';\nimport {getAxiosErrorMessage} from './get-axios-error-message';\nimport {message} from '../../i18n/message';\nimport {ToastOptions} from '@common/ui/toast/toast-store';\n\nconst defaultErrorMessage = message('There was an issue. Please try again.');\n\nexport function showHttpErrorToast(\n err: unknown,\n defaultMessage = defaultErrorMessage,\n field?: string | null,\n toastOptions?: ToastOptions\n) {\n toast.danger(getAxiosErrorMessage(err, field) || defaultMessage, {\n action: (err as any).response?.data?.action,\n ...toastOptions,\n });\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {apiClient, queryClient} from '../../http/query-client';\nimport {useUserNotifications} from '../dialog/requests/user-notifications';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\nimport {useBootstrapData} from '../../core/bootstrap-data/bootstrap-data-context';\n\ninterface Response extends BackendResponse {\n unreadCount: number;\n}\n\ninterface Payload {\n ids: string[];\n}\n\nexport function useMarkNotificationsAsRead() {\n const {data, mergeBootstrapData} = useBootstrapData();\n return useMutation({\n mutationFn: (props: Payload) => UseMarkNotificationsAsRead(props),\n onSuccess: response => {\n queryClient.invalidateQueries({queryKey: useUserNotifications.key});\n if (response.unreadCount === 0) {\n mergeBootstrapData({\n user: {...data.user!, unread_notifications_count: 0},\n });\n }\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction UseMarkNotificationsAsRead(payload: Payload): Promise {\n return apiClient\n .post('notifications/mark-as-read', payload)\n .then(r => r.data);\n}\n","import {\n createPath,\n NavigateFunction,\n resolvePath,\n useLocation,\n useNavigate as useRouterNavigate\n} from 'react-router-dom';\nimport {useCallback} from 'react';\n\nexport function useNavigate() {\n const routerNavigate = useRouterNavigate();\n const location = useLocation();\n\n return useCallback(\n (to, options) => {\n // prevent duplicates in history when navigating to the same url\n const replace =\n createPath(location) === createPath(resolvePath(to, location.pathname));\n\n routerNavigate(to, {\n ...options,\n replace: options?.replace !== false && replace,\n });\n },\n [routerNavigate, location]\n ) as NavigateFunction;\n}\n","import {Settings} from './settings';\nimport {useBootstrapData} from '../bootstrap-data/bootstrap-data-context';\n\nexport function useSettings(): Settings {\n const {\n data: {settings},\n } = useBootstrapData();\n return settings;\n}\n","import React, {JSXElementConstructor, useContext} from 'react';\nimport {GroupAddIcon} from '../icons/material/GroupAdd';\nimport {PeopleIcon} from '../icons/material/People';\nimport {FileDownloadDoneIcon} from '../icons/material/FileDownloadDone';\nimport {\n DatabaseNotification,\n DatabaseNotificationAction,\n} from './database-notification';\nimport {MixedImage} from '../ui/images/mixed-image';\nimport {Button} from '../ui/buttons/button';\nimport {SiteConfigContext} from '../core/settings/site-config-context';\nimport {Line} from './notification-line';\nimport {SvgIconProps} from '../icons/svg-icon';\nimport clsx from 'clsx';\nimport {useMarkNotificationsAsRead} from './requests/use-mark-notifications-as-read';\nimport {useNavigate} from '../utils/hooks/use-navigate';\nimport {isAbsoluteUrl} from '../utils/urls/is-absolute-url';\nimport {Link} from 'react-router-dom';\nimport {useSettings} from '../core/settings/use-settings';\n\nconst iconMap = {\n 'group-add': GroupAddIcon,\n people: PeopleIcon,\n 'export-csv': FileDownloadDoneIcon,\n} as Record>;\n\ninterface NotificationListProps {\n notifications: DatabaseNotification[];\n className?: string;\n}\nexport function NotificationList({\n notifications,\n className,\n}: NotificationListProps) {\n const {notifications: config} = useContext(SiteConfigContext);\n\n return (\n
\n {notifications.map((notification, index) => {\n const isLast = notifications.length - 1 === index;\n const Renderer =\n config?.renderMap?.[notification.type] || NotificationListItem;\n return (\n \n );\n })}\n
\n );\n}\n\nexport interface NotificationListItemProps {\n notification: DatabaseNotification;\n onActionButtonClick?: ButtonActionsProps['onActionClick'];\n lineIconRenderer?: JSXElementConstructor<{icon: string}>;\n isLast: boolean;\n}\nexport function NotificationListItem({\n notification,\n onActionButtonClick,\n lineIconRenderer,\n isLast,\n}: NotificationListItemProps) {\n const markAsRead = useMarkNotificationsAsRead();\n const navigate = useNavigate();\n const mainAction = notification.data.mainAction;\n\n const showUnreadIndicator = !notification.data.image && !notification.read_at;\n\n return (\n {\n if (!markAsRead.isPending && !notification.read_at) {\n markAsRead.mutate({ids: [notification.id]});\n }\n if (mainAction?.action) {\n if (isAbsoluteUrl(mainAction.action)) {\n window.open(mainAction.action, '_blank')?.focus();\n } else {\n navigate(mainAction.action);\n }\n }\n }}\n className={clsx(\n 'flex items-start gap-14 px-32 py-20 bg-alt relative',\n !isLast && 'border-b',\n mainAction?.action && 'cursor-pointer',\n !notification.read_at\n ? 'bg-paper hover:bg-primary/10'\n : 'hover:bg-hover',\n )}\n title={mainAction?.label ? mainAction.label : undefined}\n >\n {showUnreadIndicator && (\n
\n )}\n {notification.data.image && (\n \n )}\n
\n {notification.data.lines.map((line, index) => (\n \n ))}\n \n
\n
\n );\n}\n\ninterface ButtonActionsProps {\n notification: DatabaseNotification;\n onActionClick?: (\n e: React.MouseEvent,\n action: DatabaseNotificationAction,\n ) => void;\n}\nfunction ButtonActions({notification, onActionClick}: ButtonActionsProps) {\n const {base_url} = useSettings();\n if (!notification.data.buttonActions) return null;\n\n // if there's no action handler provided, assume action is internal url and render a link\n return (\n
\n {notification.data.buttonActions.map((action, index) => (\n {\n onActionClick?.(e, action);\n }}\n >\n {action.label}\n \n ))}\n
\n );\n}\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const DefaultFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const AudioFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const VideoFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const TextFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const PdfFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const ArchiveFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const FolderFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const ImageFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const PowerPointFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const WordFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const SpreadsheetFileIcon = createSvgIcon(\n \n \n \n);\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const SharedFolderFileIcon = createSvgIcon(\n \n \n \n);\n","import clsx from 'clsx';\nimport {DefaultFileIcon} from './icons/default-file-icon';\nimport {AudioFileIcon} from './icons/audio-file-icon';\nimport {VideoFileIcon} from './icons/video-file-icon';\nimport {TextFileIcon} from './icons/text-file-icon';\nimport {PdfFileIcon} from './icons/pdf-file-icon';\nimport {ArchiveFileIcon} from './icons/archive-file-icon';\nimport {FolderFileIcon} from './icons/folder-file-icon';\nimport {ImageFileIcon} from './icons/image-file-icon';\nimport {PowerPointFileIcon} from './icons/power-point-file-icon';\nimport {WordFileIcon} from './icons/word-file-icon';\nimport {SpreadsheetFileIcon} from './icons/spreadsheet-file-icon';\nimport {SharedFolderFileIcon} from './icons/shared-folder-file-icon';\nimport {IconSize} from '@common/icons/svg-icon';\n\ninterface Props {\n type?: string;\n mime?: string | null;\n className?: string;\n size?: IconSize;\n}\nexport function FileTypeIcon({type, mime, className, size}: Props) {\n if (!type && mime) {\n type = mime.split('/')[0];\n }\n // @ts-ignore\n const Icon = FileTypeIcons[type] || FileTypeIcons.default;\n return (\n \n );\n}\n\nconst FileTypeIcons = {\n default: DefaultFileIcon,\n audio: AudioFileIcon,\n video: VideoFileIcon,\n text: TextFileIcon,\n pdf: PdfFileIcon,\n archive: ArchiveFileIcon,\n folder: FolderFileIcon,\n sharedFolder: SharedFolderFileIcon,\n image: ImageFileIcon,\n powerPoint: PowerPointFileIcon,\n word: WordFileIcon,\n spreadsheet: SpreadsheetFileIcon,\n};\n","import {\n NotificationListItem,\n NotificationListItemProps,\n} from '@common/notifications/notification-list';\nimport {FileTypeIcon} from '@common/uploads/file-type-icon/file-type-icon';\n\nexport function FileEntrySharedNotificationRenderer(\n props: NotificationListItemProps\n) {\n return ;\n}\n\ninterface IconRendererProps {\n icon: string;\n}\nfunction IconRenderer({icon}: IconRendererProps) {\n return ;\n}\n","import {SiteConfigContextValue} from '@common/core/settings/site-config-context';\nimport {message} from '@common/i18n/message';\nimport filePreviewSrc from './admin/verts/file-preview.png';\nimport driveSrc from './admin/verts/drive.png';\nimport landingTopSrc from './admin/verts/landing-top.png';\nimport {FileEntrySharedNotificationRenderer} from './drive/notifications/file-entry-shared-notification-renderer';\n\nconst fileEntrySharedNotif = 'App\\\\Notifications\\\\FileEntrySharedNotif';\n\nexport const SiteConfig: Partial = {\n notifications: {\n renderMap: {\n [fileEntrySharedNotif]: FileEntrySharedNotificationRenderer,\n },\n },\n homepage: {\n options: [{label: message('Landing page'), value: 'landingPage'}],\n },\n auth: {\n redirectUri: '/drive',\n adminRedirectUri: '/drive',\n },\n tags: {\n types: [{name: 'label', system: true}],\n },\n admin: {\n ads: [\n {\n slot: 'ads.file-preview',\n description: message(\n 'This ad will appear on shared file preview page.'\n ),\n image: filePreviewSrc,\n },\n {\n slot: 'ads.drive',\n description: message('This ad will appear on user drive page.'),\n image: driveSrc,\n },\n {\n slot: 'ads.landing-top',\n description: message(\n 'This ad will appear at the top of the landing page.'\n ),\n image: landingTopSrc,\n },\n ],\n },\n demo: {\n loginPageDefaults: 'randomAccount',\n },\n};\n","export const WorkspaceQueryKeys = {\n fetchUserWorkspaces: ['user-workspaces'],\n workspaceWithMembers: (id: number) => ['workspace-with-members', id],\n};\n","import {useQuery} from '@tanstack/react-query';\nimport {WorkspaceQueryKeys} from './requests/workspace-query-keys';\nimport {Workspace} from './types/workspace';\nimport {BackendResponse} from '../http/backend-response/backend-response';\nimport {apiClient} from '../http/query-client';\n\nexport interface FetchUserWorkspacesResponse extends BackendResponse {\n workspaces: Workspace[];\n}\n\nexport const PersonalWorkspace: Workspace = {\n name: 'Default',\n default: true,\n id: 0,\n members_count: 1,\n};\n\nfunction fetchUserWorkspaces(): Promise {\n return apiClient.get(`me/workspaces`).then(response => response.data);\n}\n\nfunction addPersonalWorkspaceToResponse(response: FetchUserWorkspacesResponse) {\n return [PersonalWorkspace, ...response.workspaces];\n}\n\nexport function useUserWorkspaces() {\n return useQuery({\n queryKey: WorkspaceQueryKeys.fetchUserWorkspaces,\n queryFn: fetchUserWorkspaces,\n placeholderData: {workspaces: []},\n select: addPersonalWorkspaceToResponse,\n });\n}\n","export function isSsr() {\n return import.meta.env.SSR;\n}\n","import {useCallback, useEffect, useState} from 'react';\nimport {isSsr} from '@common/utils/dom/is-ssr';\n\ninterface Options {\n days?: number;\n path?: string;\n domain?: string;\n SameSite?: 'None' | 'Lax' | 'Strict';\n Secure?: boolean;\n HttpOnly?: boolean;\n}\n\n// used to notify different instances of useCookie hook about cookie changes\nconst listeners = new Set<{name: string; callback: any}>();\nconst listenForCookieChange = (\n name: string,\n callback: (value: string) => void\n) => {\n if (isSsr()) return () => {};\n const listener = {name, callback};\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n};\n\nexport function stringifyOptions(options: Options) {\n return Object.keys(options).reduce((acc, _key) => {\n const key = _key as keyof Options;\n if (key === 'days') {\n return acc;\n } else {\n if (options[key] === false) {\n return acc;\n } else if (options[key] === true) {\n return `${acc}; ${key}`;\n } else {\n return `${acc}; ${key}=${options[key]}`;\n }\n }\n }, '');\n}\n\nexport const setCookie = (name: string, value: string, options?: Options) => {\n if (isSsr()) return;\n\n const optionsWithDefaults = {\n days: 7,\n path: '/',\n ...options,\n };\n\n const expires = new Date(\n Date.now() + optionsWithDefaults.days * 864e5\n ).toUTCString();\n\n document.cookie =\n name +\n '=' +\n encodeURIComponent(value) +\n '; expires=' +\n expires +\n stringifyOptions(optionsWithDefaults);\n\n listeners.forEach(listener => {\n if (listener.name === name) {\n listener.callback(value);\n }\n });\n};\n\nexport const getCookie = (name: string, initialValue = '') => {\n return (\n (!isSsr() &&\n document.cookie.split('; ').reduce((r, v) => {\n const parts = v.split('=');\n return parts[0] === name ? decodeURIComponent(parts[1]) : r;\n }, '')) ||\n initialValue\n );\n};\n\nexport function useCookie(key: string, initialValue?: string) {\n const [item, setItem] = useState(() => {\n return getCookie(key, initialValue);\n });\n\n useEffect(() => {\n return listenForCookieChange(key, value => {\n setItem(value);\n });\n }, [key]);\n\n const updateItem = useCallback(\n (value: string, options?: Options) => {\n setItem(value);\n setCookie(key, value, options);\n },\n [key]\n );\n\n return [item, updateItem] as const;\n}\n","import React, {useContext, useEffect, useMemo} from 'react';\nimport {Workspace} from './types/workspace';\nimport {PersonalWorkspace, useUserWorkspaces} from './user-workspaces';\nimport {setActiveWorkspaceId} from './active-workspace-id';\nimport {useCookie} from '@common/utils/hooks/use-cookie';\n\nexport interface ActiveWorkspaceIdContextValue {\n workspaceId: number | null;\n setWorkspaceId: (id: number) => void;\n}\n\n// add default context value so it does not error out, if there's no context provider\nexport const ActiveWorkspaceIdContext =\n React.createContext({\n // set default as null, so it's not sent via query params in admin and\n // other places if component is not wrapped in workspace context explicitly\n workspaceId: null,\n setWorkspaceId: () => {},\n });\n\nexport function useActiveWorkspaceId(): ActiveWorkspaceIdContextValue {\n return useContext(ActiveWorkspaceIdContext);\n}\n\nexport function useActiveWorkspace(): Workspace | null | undefined {\n const {workspaceId} = useActiveWorkspaceId();\n const query = useUserWorkspaces();\n if (query.data) {\n return query.data.find(workspace => workspace.id === workspaceId);\n }\n return null;\n}\n\ninterface ActiveWorkspaceProviderProps {\n children: any;\n}\nexport function ActiveWorkspaceProvider({\n children,\n}: ActiveWorkspaceProviderProps) {\n const [workspaceId, setCookieId] = useCookie(\n 'activeWorkspaceId',\n `${PersonalWorkspace.id}`\n );\n\n useEffect(() => {\n setActiveWorkspaceId(parseInt(workspaceId));\n // clear workspace id when unmounting workspace provider\n return () => {\n setActiveWorkspaceId(0);\n };\n }, [workspaceId]);\n\n const contextValue: ActiveWorkspaceIdContextValue = useMemo(() => {\n return {\n workspaceId: parseInt(workspaceId),\n setWorkspaceId: (id: number) => {\n setCookieId(`${id}`);\n },\n };\n }, [workspaceId, setCookieId]);\n\n return (\n \n {children}\n \n );\n}\n","import axios from 'axios';\nimport {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {toast} from '../../ui/toast/toast';\nimport {apiClient, queryClient} from '../../http/query-client';\nimport {Workspace} from '../types/workspace';\nimport {WorkspaceQueryKeys} from './workspace-query-keys';\nimport {useActiveWorkspaceId} from '../active-workspace-id-context';\nimport {useUserNotifications} from '../../notifications/dialog/requests/user-notifications';\nimport {message} from '../../i18n/message';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {\n workspace: Workspace;\n}\n\ninterface Props {\n inviteId: string;\n}\n\nexport function useJoinWorkspace() {\n const {setWorkspaceId} = useActiveWorkspaceId() || {};\n return useMutation({\n mutationFn: (props: Props) => joinWorkspace(props),\n onSuccess: response => {\n toast(message('Joined workspace'));\n setWorkspaceId(response.workspace.id);\n queryClient.invalidateQueries({\n queryKey: WorkspaceQueryKeys.fetchUserWorkspaces,\n });\n queryClient.invalidateQueries({queryKey: useUserNotifications.key});\n },\n onError: e => {\n if (axios.isAxiosError(e) && e.response && e.response.status === 404) {\n queryClient.invalidateQueries({queryKey: useUserNotifications.key});\n toast.danger(message('This invite is no longer valid'));\n } else {\n showHttpErrorToast(e);\n }\n },\n });\n}\n\nfunction joinWorkspace({inviteId}: Props): Promise {\n return apiClient.get(`workspace/join/${inviteId}`).then(r => r.data);\n}\n","import axios from 'axios';\nimport {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {toast} from '../../ui/toast/toast';\nimport {apiClient, queryClient} from '../../http/query-client';\nimport {useUserNotifications} from '../../notifications/dialog/requests/user-notifications';\nimport {message} from '../../i18n/message';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\ninterface Props {\n inviteId: string;\n}\n\nfunction deleteInvite({inviteId}: Props): Promise {\n return apiClient.delete(`workspace/invite/${inviteId}`).then(r => r.data);\n}\n\nexport function useDeleteInvite() {\n return useMutation({\n mutationFn: (props: Props) => deleteInvite(props),\n onSuccess: () => {\n queryClient.invalidateQueries({queryKey: useUserNotifications.key});\n toast(message('Declined workspace invitation'));\n },\n onError: e => {\n if (axios.isAxiosError(e) && e.response && e.response.status === 404) {\n queryClient.invalidateQueries({queryKey: useUserNotifications.key});\n toast.danger(message('This invite is no longer valid'));\n } else {\n showHttpErrorToast(e);\n }\n },\n });\n}\n","import React, {ComponentPropsWithRef, useContext} from 'react';\n\nexport type DialogType = 'modal' | 'popover' | 'tray';\n\nexport interface DialogContextValue {\n labelId: string;\n descriptionId: string;\n type: DialogType;\n isDismissable?: boolean;\n close: (value?: any) => void;\n formId: string;\n dialogProps: ComponentPropsWithRef<'div'>;\n disableInitialTransition?: boolean;\n}\n\nexport const DialogContext = React.createContext(null!);\n\nexport function useDialogContext() {\n return useContext(DialogContext);\n}\n","import {\n NotificationListItem,\n NotificationListItemProps,\n} from '../../notifications/notification-list';\nimport {\n DatabaseNotification,\n DatabaseNotificationData,\n} from '../../notifications/database-notification';\nimport {useJoinWorkspace} from '../requests/join-workspace';\nimport {useDeleteInvite} from '../requests/delete-invite';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\n\nexport interface WorkspaceInviteNotification extends DatabaseNotification {\n data: DatabaseNotificationData & {inviteId: string};\n}\n\nexport function WorkspaceInviteNotificationRenderer(\n props: NotificationListItemProps\n) {\n const {notification} = props;\n const joinWorkspace = useJoinWorkspace();\n const deleteInvite = useDeleteInvite();\n const dialogContextValue = useDialogContext();\n\n return (\n {\n const data = (notification as WorkspaceInviteNotification).data;\n if (action === 'join') {\n joinWorkspace.mutate({\n inviteId: data.inviteId,\n });\n }\n if (action === 'decline') {\n deleteInvite.mutate({\n inviteId: data.inviteId,\n });\n }\n dialogContextValue?.close();\n }}\n />\n );\n}\n","import {SiteConfigContextValue} from './site-config-context';\nimport {WorkspaceInviteNotificationRenderer} from '../../workspace/notifications/workspace-invite-notification-renderer';\nimport {message} from '../../i18n/message';\n\nconst workspaceInviteNotif =\n 'Common\\\\Workspaces\\\\Notifications\\\\WorkspaceInvitation';\n\nexport const BaseSiteConfig: SiteConfigContextValue = {\n auth: {\n redirectUri: '/',\n adminRedirectUri: '/admin',\n },\n tags: {\n types: [{name: 'custom'}],\n },\n customPages: {\n types: [{type: 'default', label: message('Default')}],\n },\n notifications: {\n renderMap: {\n [workspaceInviteNotif]: WorkspaceInviteNotificationRenderer,\n },\n },\n admin: {\n ads: [],\n },\n demo: {\n loginPageDefaults: 'singleAccount',\n },\n homepage: {\n options: [\n {label: message('Login page'), value: 'loginPage'},\n {label: message('Registration page'), value: 'registerPage'},\n ],\n },\n};\n","export let rootEl = (\n typeof document !== 'undefined'\n ? document.getElementById('root') ?? document.body\n : undefined\n) as HTMLElement;\n\nexport let themeEl = (\n typeof document !== 'undefined' ? document.documentElement : undefined\n) as HTMLElement;\n\nexport function setRootEl(el: HTMLElement) {\n rootEl = el;\n themeEl = el;\n}\n","import {themeEl} from '@common/core/root-el';\n\nexport function setThemeColor(key: string, value: string) {\n themeEl?.style.setProperty(key, value);\n}\n","import {CssTheme} from '../css-theme';\nimport {setThemeColor} from './set-theme-color';\nimport {themeEl} from '@common/core/root-el';\n\nexport function applyThemeToDom(theme: CssTheme) {\n Object.entries(theme.colors).forEach(([key, value]) => {\n setThemeColor(key, value);\n });\n if (theme.is_dark) {\n themeEl.classList.add('dark');\n } else {\n themeEl.classList.remove('dark');\n }\n}\n","import {createContext, useContext} from 'react';\nimport {CssTheme} from './css-theme';\n\nexport type ThemeId = 'light' | 'dark' | number;\n\nexport interface ThemeSelectorContextValue {\n allThemes: CssTheme[];\n selectedTheme: CssTheme;\n selectTheme: (themeId: ThemeId) => void;\n}\n\nexport const ThemeSelectorContext = createContext(\n null!\n);\n\nexport function useThemeSelector() {\n return useContext(ThemeSelectorContext);\n}\n","import React, {useMemo} from 'react';\nimport {applyThemeToDom} from '../ui/themes/utils/apply-theme-to-dom';\nimport {\n ThemeId,\n ThemeSelectorContext,\n ThemeSelectorContextValue,\n} from '../ui/themes/theme-selector-context';\nimport {CssTheme} from '../ui/themes/css-theme';\nimport {useSettings} from './settings/use-settings';\nimport {useBootstrapData} from './bootstrap-data/bootstrap-data-context';\nimport {useCookie} from '@common/utils/hooks/use-cookie';\n\nconst STORAGE_KEY = 'be-active-theme';\n\ninterface ThemeProviderProps {\n children: any;\n}\nexport function ThemeProvider({children}: ThemeProviderProps) {\n const {\n themes: {user_change, default_id},\n } = useSettings();\n const {data} = useBootstrapData();\n const allThemes = useMemo(() => data.themes.all || [], [data.themes.all]);\n const initialThemeId = data.themes.selectedThemeId || undefined;\n\n const [selectedThemeId, setSelectedThemeId] = useCookie(\n STORAGE_KEY,\n `${initialThemeId}`\n );\n\n let selectedTheme = user_change\n ? allThemes.find(t => t.id == selectedThemeId)\n : allThemes.find(t => t.id == default_id);\n if (!selectedTheme) {\n selectedTheme = allThemes[0];\n }\n\n const contextValue: ThemeSelectorContextValue = useMemo(() => {\n return {\n allThemes,\n selectedTheme: selectedTheme!,\n selectTheme: (id: ThemeId) => {\n if (!user_change) return;\n const theme = findTheme(allThemes, id);\n if (theme) {\n setSelectedThemeId(`${theme.id}`);\n applyThemeToDom(theme);\n }\n },\n };\n }, [allThemes, selectedTheme, setSelectedThemeId, user_change]);\n\n return (\n \n {children}\n \n );\n}\n\nfunction findTheme(themes: CssTheme[], id: ThemeId) {\n return themes.find(t => {\n if (id === 'light') {\n return t.default_light === true;\n }\n if (id === 'dark') {\n return t.default_dark === true;\n }\n return t.id === id;\n });\n}\n","import React, {useMemo} from 'react';\nimport {\n invalidateBootstrapData,\n mergeBootstrapData,\n setBootstrapData,\n useBackendBootstrapData,\n} from './use-backend-bootstrap-data';\nimport {\n BoostrapDataContext,\n BoostrapDataContextValue,\n} from './bootstrap-data-context';\n\ninterface BootstrapDataProviderProps {\n children: any;\n}\nexport function BootstrapDataProvider({children}: BootstrapDataProviderProps) {\n const {data} = useBackendBootstrapData();\n\n const value: BoostrapDataContextValue = useMemo(() => {\n return {\n data: data,\n setBootstrapData: setBootstrapData,\n mergeBootstrapData: mergeBootstrapData,\n invalidateBootstrapData: invalidateBootstrapData,\n };\n }, [data]);\n\n return (\n \n {children}\n \n );\n}\n","import React from 'react';\nimport {QueryClientProvider} from '@tanstack/react-query';\nimport {domAnimation, LazyMotion} from 'framer-motion';\nimport {queryClient} from '../http/query-client';\nimport {SiteConfigContext} from './settings/site-config-context';\nimport {SiteConfig} from '@app/site-config';\nimport deepMerge from 'deepmerge';\nimport {BaseSiteConfig} from './settings/base-site-config';\nimport {ThemeProvider} from './theme-provider';\nimport {BootstrapDataProvider} from './bootstrap-data/bootstrap-data-provider';\n\ninterface ProvidersProps {\n children: any;\n}\n\nconst mergedConfig = deepMerge(BaseSiteConfig, SiteConfig);\n\nexport function CommonProvider({children}: ProvidersProps) {\n return (\n \n \n \n \n {children}\n \n \n \n \n );\n}\n","import {useEffect, useState} from 'react';\n\ninterface StoreEvent {\n detail: {\n key: string;\n newValue: any;\n };\n}\n\nexport function useLocalStorage(key: string, initialValue: T | null = null) {\n const [storedValue, setStoredValue] = useState(() => {\n return getFromLocalStorage(key, initialValue);\n });\n\n const setValue = (value: T | ((val: T) => T)) => {\n const valueToStore = value instanceof Function ? value(storedValue) : value;\n setStoredValue(valueToStore);\n setInLocalStorage(key, valueToStore);\n window.dispatchEvent(\n new CustomEvent('storage', {\n detail: {key, newValue: valueToStore},\n })\n );\n };\n\n // update state value using custom storage event. This will re-render\n // component even if local storage value was set from different hook instance\n useEffect(() => {\n const handleStorageChange = (event: StoreEvent) => {\n if (event.detail?.key === key) {\n setStoredValue(event.detail.newValue);\n }\n };\n window.addEventListener('storage', handleStorageChange as any);\n return () =>\n window.removeEventListener('storage', handleStorageChange as any);\n }, [key]);\n\n return [storedValue, setValue] as const;\n}\n\nexport function getFromLocalStorage(\n key: string,\n initialValue: T | null = null\n) {\n if (typeof window === 'undefined') {\n return initialValue;\n }\n try {\n const item = window.localStorage.getItem(key);\n return item != null ? JSON.parse(item) : initialValue;\n } catch (error) {\n return initialValue;\n }\n}\n\nexport function setInLocalStorage(key: string, value: T) {\n try {\n if (typeof window !== 'undefined') {\n window.localStorage.setItem(key, JSON.stringify(value));\n }\n } catch (error) {\n //\n }\n}\n\nexport function removeFromLocalStorage(key: string) {\n try {\n if (typeof window !== 'undefined') {\n window.localStorage.removeItem(key);\n }\n } catch (error) {\n //\n }\n}\n","import {User} from './user';\nimport {useCallback, useContext} from 'react';\nimport {SiteConfigContext} from '../core/settings/site-config-context';\nimport {getFromLocalStorage} from '../utils/hooks/local-storage';\nimport {useBootstrapData} from '../core/bootstrap-data/bootstrap-data-context';\nimport {Permission} from '@common/auth/permission';\n\ninterface UseAuthReturn {\n user: User | null;\n hasPermission: (permission: string) => boolean;\n getPermission: (permission: string) => Permission | undefined;\n getRestrictionValue: (\n permission: string,\n restriction: string\n ) => string | number | boolean | undefined | null;\n isLoggedIn: boolean;\n isSubscribed: boolean;\n getRedirectUri: () => string;\n}\nexport function useAuth(): UseAuthReturn {\n const {\n data: {user, guest_role},\n } = useBootstrapData();\n const {\n auth: {redirectUri = '/'},\n } = useContext(SiteConfigContext);\n\n const getPermission = useCallback(\n (name: string): Permission | undefined => {\n const permissions = user?.permissions || guest_role?.permissions;\n if (!permissions) return;\n return permissions.find(p => p.name === name);\n },\n [user?.permissions, guest_role?.permissions]\n );\n\n const getRestrictionValue = useCallback(\n (\n permissionName: string,\n restrictionName: string\n ): string | number | boolean | undefined | null => {\n const permission = getPermission(permissionName);\n let restrictionValue = null;\n if (permission) {\n const restriction = permission.restrictions.find(\n r => r.name === restrictionName\n );\n restrictionValue = restriction ? restriction.value : undefined;\n }\n return restrictionValue;\n },\n [getPermission]\n );\n\n const hasPermission = useCallback(\n (name: string): boolean => {\n const permissions = user?.permissions || guest_role?.permissions;\n\n const isAdmin = permissions?.find(p => p.name === 'admin') != null;\n return isAdmin || getPermission(name) != null;\n },\n [user?.permissions, guest_role?.permissions, getPermission]\n );\n\n const isSubscribed = user?.subscriptions?.find(sub => sub.valid) != null;\n\n const getRedirectUri = useCallback(() => {\n const onboarding = getFromLocalStorage('be.onboarding.selected');\n if (onboarding) {\n return `/checkout/${onboarding.productId}/${onboarding.priceId}`;\n }\n return redirectUri;\n }, [redirectUri]);\n\n return {\n user,\n hasPermission,\n getPermission,\n getRestrictionValue,\n isLoggedIn: !!user,\n isSubscribed,\n // where to redirect user after successful login\n getRedirectUri,\n };\n}\n","import React, {cloneElement, forwardRef, ReactElement} from 'react';\nimport clsx from 'clsx';\nimport {ButtonSize, getButtonSizeStyle} from './button-size';\nimport {ButtonBase, ButtonBaseProps} from './button-base';\nimport {BadgeProps} from '@common/ui/badge/badge';\n\nexport interface IconButtonProps extends ButtonBaseProps {\n children: ReactElement;\n padding?: string;\n size?: ButtonSize | null;\n iconSize?: ButtonSize | null;\n equalWidth?: boolean;\n badge?: ReactElement;\n}\nexport const IconButton = forwardRef(\n (\n {\n children,\n size = 'md',\n // only set icon size based on button size if \"ButtonSize\" is passed in and not custom className\n iconSize = size && size.length <= 3 ? size : 'md',\n variant = 'text',\n radius = 'rounded-full',\n className,\n padding,\n equalWidth = true,\n badge,\n ...other\n },\n ref\n ) => {\n const mergedClassName = clsx(\n getButtonSizeStyle(size, {padding, equalWidth, variant}),\n className,\n badge && 'relative'\n );\n\n return (\n \n {cloneElement(children, {size: iconSize})}\n {badge}\n \n );\n }\n);\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const CloseIcon = createSvgIcon(\n \n, 'CloseOutlined');\n","import {MessageDescriptor} from './message-descriptor';\nimport {Trans} from './trans';\nimport {Fragment} from 'react';\n\ninterface Props {\n value?: string | MessageDescriptor | null;\n}\nexport function MixedText({value}: Props) {\n if (!value) {\n return null;\n }\n if (typeof value === 'string') {\n return {value};\n }\n return ;\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const ErrorOutlineIcon = createSvgIcon(\n \n, 'ErrorOutlineOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const CheckCircleIcon = createSvgIcon(\n \n, 'CheckCircleOutlined');\n","export function clamp(num: number, min: number, max: number) {\n return Math.min(Math.max(num, min), max);\n}\n","import {NumberFormatOptions, NumberFormatter} from '@internationalized/number';\nimport {useMemo} from 'react';\nimport {useSelectedLocale} from './selected-locale';\n\nexport function useNumberFormatter(\n options: NumberFormatOptions = {}\n): Intl.NumberFormat {\n const {localeCode} = useSelectedLocale();\n return useMemo(\n () => new NumberFormatter(localeCode, options),\n [localeCode, options]\n );\n}\n","import React, {ComponentPropsWithoutRef, CSSProperties} from 'react';\nimport clsx from 'clsx';\nimport {clamp} from '../../utils/number/clamp';\nimport {useNumberFormatter} from '../../i18n/use-number-formatter';\n\nexport interface ProgressCircleProps extends ComponentPropsWithoutRef<'div'> {\n value?: number;\n minValue?: number;\n maxValue?: number;\n size?: 'xs' | 'sm' | 'md' | 'lg' | string;\n isIndeterminate?: boolean;\n className?: string;\n position?: string;\n trackColor?: string;\n fillColor?: string;\n}\nexport const ProgressCircle = React.forwardRef<\n HTMLDivElement,\n ProgressCircleProps\n>((props, ref) => {\n let {\n value = 0,\n minValue = 0,\n maxValue = 100,\n size = 'md',\n isIndeterminate = false,\n className,\n position = 'relative',\n trackColor,\n fillColor = 'border-primary',\n ...domProps\n } = props;\n\n value = clamp(value, minValue, maxValue);\n const circleSize = getCircleStyle(size);\n\n const percentage = (value - minValue) / (maxValue - minValue);\n const formatter = useNumberFormatter({style: 'percent'});\n\n let valueLabel = '';\n if (!isIndeterminate && !valueLabel) {\n valueLabel = formatter.format(percentage);\n }\n\n const subMask1Style: CSSProperties = {};\n const subMask2Style: CSSProperties = {};\n if (!isIndeterminate) {\n const percentage = ((value - minValue) / (maxValue - minValue)) * 100;\n let angle;\n if (percentage > 0 && percentage <= 50) {\n angle = -180 + (percentage / 50) * 180;\n subMask1Style.transform = `rotate(${angle}deg)`;\n subMask2Style.transform = 'rotate(-180deg)';\n } else if (percentage > 50) {\n angle = -180 + ((percentage - 50) / 50) * 180;\n subMask1Style.transform = 'rotate(0deg)';\n subMask2Style.transform = `rotate(${angle}deg)`;\n }\n }\n\n return (\n \n
\n \n \n \n
\n \n );\n});\n\ninterface FillMaskProps {\n className?: string;\n circleSize?: string;\n subMaskStyle: CSSProperties;\n subMaskClassName: string;\n isIndeterminate?: boolean;\n fillColor?: string;\n}\nfunction FillMask({\n subMaskStyle,\n subMaskClassName,\n className,\n circleSize,\n isIndeterminate,\n fillColor,\n}: FillMaskProps) {\n return (\n \n \n
\n
\n \n );\n}\n\nfunction getCircleStyle(size: ProgressCircleProps['size']) {\n switch (size) {\n case 'xs':\n return 'w-20 h-20';\n case 'sm':\n return 'w-24 h-24';\n case 'md':\n return 'w-32 h-32';\n case 'lg':\n return 'w-42 h-42';\n default:\n return size;\n }\n}\n","import {AnimatePresence, m, Target, TargetAndTransition} from 'framer-motion';\nimport React from 'react';\nimport clsx from 'clsx';\nimport {IconButton} from '../buttons/icon-button';\nimport {CloseIcon} from '../../icons/material/Close';\nimport {MixedText} from '../../i18n/mixed-text';\nimport {Button} from '../buttons/button';\nimport {toastState, useToastStore} from './toast-store';\nimport {Link} from 'react-router-dom';\nimport {ErrorOutlineIcon} from '../../icons/material/ErrorOutline';\nimport {CheckCircleIcon} from '../../icons/material/CheckCircle';\nimport {ProgressCircle} from '@common/ui/progress/progress-circle';\n\nconst initial: Target = {opacity: 0, y: 50, scale: 0.3};\nconst animate: TargetAndTransition = {opacity: 1, y: 0, scale: 1};\nconst exit: TargetAndTransition = {\n opacity: 0,\n scale: 0.5,\n};\n\nexport function ToastContainer() {\n const toasts = useToastStore(s => s.toasts);\n\n return (\n
\n \n {toasts.map(toast => (\n \n toast.timer?.pause()}\n onPointerLeave={() => toast.timer?.resume()}\n role=\"alert\"\n aria-live={toast.type === 'danger' ? 'assertive' : 'polite'}\n >\n {toast.type === 'danger' && (\n \n )}\n {toast.type === 'loading' && (\n \n )}\n {toast.type === 'positive' && (\n \n )}\n\n \n \n
\n\n {toast.action && (\n toast.timer?.pause()}\n onBlur={() => toast.timer?.resume()}\n onClick={() => toastState().remove(toast.id)}\n elementType={Link}\n to={toast.action.action}\n >\n \n \n )}\n {toast.type !== 'loading' && (\n toast.timer?.pause()}\n onBlur={() => toast.timer?.resume()}\n type=\"button\"\n className=\"flex-shrink-0\"\n onClick={() => {\n toastState().remove(toast.id);\n }}\n size=\"sm\"\n >\n \n \n )}\n \n \n ))}\n \n \n );\n}\n","import {useQuery} from '@tanstack/react-query';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {User} from '../user';\nimport {apiClient} from '../../http/query-client';\n\nexport interface FetchUseUserResponse extends BackendResponse {\n user: User;\n}\n\ninterface Params {\n with: string[];\n}\n\ntype UserId = number | string | 'me';\nconst queryKey = (id: UserId, params?: Params) => {\n const key: any[] = ['users', `${id}`];\n if (params) {\n key.push(params);\n }\n return key;\n};\n\nexport function useUser(id: UserId, params?: Params) {\n return useQuery({\n queryKey: queryKey(id, params),\n queryFn: () => fetchUser(id, params),\n });\n}\n\nfunction fetchUser(id: UserId, params?: Params): Promise {\n return apiClient.get(`users/${id}`, {params}).then(response => response.data);\n}\n","export default \"__VITE_ASSET__29738208__\"","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {toast} from '../../ui/toast/toast';\nimport {message} from '../../i18n/message';\nimport {apiClient} from '../../http/query-client';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {\n message: string;\n}\n\nexport interface ResendConfirmEmailPayload {\n email: string;\n}\n\nexport function useResendVerificationEmail() {\n return useMutation({\n mutationFn: resendEmail,\n onSuccess: () => {\n toast(message('Email sent'));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction resendEmail(payload: ResendConfirmEmailPayload): Promise {\n return apiClient\n .post('auth/email/verification-notification', payload)\n .then(response => response.data);\n}\n","import {useThemeSelector} from './theme-selector-context';\n\nexport function useIsDarkMode(): boolean {\n const {selectedTheme} = useThemeSelector();\n return selectedTheme.is_dark ?? false;\n}\n","import {isSsr} from '@common/utils/dom/is-ssr';\n\nexport function useAppearanceEditorMode() {\n return {\n isAppearanceEditorActive:\n !isSsr() &&\n ((window.frameElement as HTMLIFrameElement) || undefined)?.src.includes(\n 'appearanceEditor=true'\n ),\n };\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {useNavigate} from '../../utils/hooks/use-navigate';\nimport {apiClient, queryClient} from '../../http/query-client';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\nimport {useAppearanceEditorMode} from '../../admin/appearance/commands/use-appearance-editor-mode';\nimport {message} from '../../i18n/message';\nimport {useBootstrapData} from '../../core/bootstrap-data/bootstrap-data-context';\n\ninterface Response extends BackendResponse {\n bootstrapData: string;\n}\n\nconst appearanceMessage = \"Can't logout while in appearance editor.\";\n\nexport function useLogout() {\n const navigate = useNavigate();\n const {isAppearanceEditorActive} = useAppearanceEditorMode();\n const {setBootstrapData} = useBootstrapData();\n return useMutation({\n mutationFn: () => (isAppearanceEditorActive ? noopLogout() : logout()),\n onSuccess: response => {\n // need to update bootstrap data in order for redirect to login page to work\n setBootstrapData(response.bootstrapData);\n queryClient.clear();\n navigate('/login');\n\n // need to clear query client and then set bootstrap data again immediately,\n // because there's no way to clear everything except one in react query\n queryClient.clear();\n setBootstrapData(response.bootstrapData);\n },\n onError: err =>\n showHttpErrorToast(\n err,\n isAppearanceEditorActive ? message(appearanceMessage) : undefined,\n ),\n });\n}\n\nfunction logout(): Promise {\n return apiClient.post('auth/logout').then(r => r.data);\n}\n\nfunction noopLogout() {\n return Promise.reject(appearanceMessage);\n}\n","import {useUser} from '@common/auth/ui/use-user';\nimport {Trans} from '@common/i18n/trans';\nimport mailSentSvg from './mail-sent.svg';\nimport {SvgImage} from '@common/ui/images/svg-image/svg-image';\nimport {Button} from '@common/ui/buttons/button';\nimport {useResendVerificationEmail} from '@common/auth/requests/use-resend-verification-email';\nimport {useIsDarkMode} from '@common/ui/themes/use-is-dark-mode';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {useLogout} from '@common/auth/requests/logout';\n\nexport function EmailVerificationPage() {\n const {data} = useUser('me');\n const resendEmail = useResendVerificationEmail();\n const {\n branding: {logo_light, logo_dark},\n } = useSettings();\n const isDarkMode = useIsDarkMode();\n const logoSrc = isDarkMode ? logo_light : logo_dark;\n const logout = useLogout();\n\n return (\n
\n {logoSrc && (\n \n )}\n
\n \n

\n \n

\n
\n \n
\n
\n \n
\n
\n {\n resendEmail.mutate({email: data!.user.email});\n }}\n >\n \n \n \n
\n
\n
\n );\n}\n","import {create} from 'zustand';\nimport {immer} from 'zustand/middleware/immer';\nimport React, {JSXElementConstructor} from 'react';\n\ninterface DialogStore<\n C extends JSXElementConstructor = JSXElementConstructor,\n D = React.ComponentProps\n> {\n dialog: C | null;\n data: D;\n openDialog: (dialog: C, data?: D) => Promise;\n closeActiveDialog: (value: any) => void;\n resolveClosePromise: null | ((value: any) => void);\n}\n\nexport const useDialogStore = create()(\n immer((set, get) => ({\n dialog: null,\n data: undefined,\n resolveClosePromise: null,\n openDialog: (dialog, data) => {\n return new Promise(resolve => {\n set(state => {\n state.dialog = dialog;\n state.data = data;\n state.resolveClosePromise = resolve;\n });\n });\n },\n closeActiveDialog: value => {\n get().resolveClosePromise?.(value);\n set(state => {\n state.dialog = null;\n state.data = undefined;\n state.resolveClosePromise = null;\n });\n },\n }))\n);\n\nexport const openDialog = useDialogStore.getState().openDialog;\nexport const closeDialog = (value?: any) => {\n useDialogStore.getState().closeActiveDialog(value);\n};\n","import {\n arrow,\n autoUpdate,\n flip,\n offset as offsetMiddleware,\n OffsetOptions,\n Placement,\n ReferenceType,\n shift,\n size,\n useFloating,\n} from '@floating-ui/react-dom';\nimport {CSSProperties, Ref, useMemo, useRef} from 'react';\nimport {mergeRefs} from 'react-merge-refs';\nimport {UseFloatingOptions} from '@floating-ui/react-dom/src/types';\n\ninterface Props {\n floatingWidth?: 'auto' | 'matchTrigger';\n ref?: Ref;\n disablePositioning?: boolean;\n placement?: Placement;\n offset?: OffsetOptions;\n showArrow?: boolean;\n maxHeight?: number;\n shiftCrossAxis?: boolean;\n fallbackPlacements?: Placement[];\n}\nexport function useFloatingPosition({\n floatingWidth,\n ref,\n disablePositioning = false,\n placement = 'bottom',\n offset = 2,\n showArrow = false,\n maxHeight,\n shiftCrossAxis = true,\n fallbackPlacements,\n}: Props) {\n const arrowRef = useRef(null);\n\n const floatingConfig: UseFloatingOptions = {placement, strategy: 'fixed'};\n\n if (!disablePositioning) {\n floatingConfig.whileElementsMounted = autoUpdate;\n floatingConfig.middleware = [\n offsetMiddleware(offset),\n shift({padding: 16, crossAxis: shiftCrossAxis, mainAxis: true}),\n flip({\n padding: 16,\n fallbackPlacements,\n }),\n size({\n apply({rects, availableHeight, availableWidth, elements}) {\n if (floatingWidth === 'matchTrigger' && maxHeight != null) {\n Object.assign(elements.floating.style, {\n width: `${rects.reference.width}px`,\n maxWidth: `${availableWidth}`,\n maxHeight: `${Math.min(availableHeight, maxHeight)}px`,\n });\n } else if (maxHeight != null) {\n Object.assign(elements.floating.style, {\n maxHeight: `${Math.min(availableHeight, maxHeight)}px`,\n });\n }\n },\n padding: 16,\n }),\n ];\n if (showArrow) {\n floatingConfig.middleware.push(arrow({element: arrowRef}));\n }\n }\n\n const floatingProps = useFloating(floatingConfig);\n\n const mergedReferenceRef = useMemo(\n () => mergeRefs([ref!, floatingProps.refs.setReference]),\n [floatingProps.refs.setReference, ref]\n );\n\n const {x: arrowX, y: arrowY} = floatingProps.middlewareData.arrow || {};\n\n const staticSide = {\n top: 'bottom',\n right: 'left',\n bottom: 'top',\n left: 'right',\n }[floatingProps.placement.split('-')[0]]!;\n\n const arrowStyle: CSSProperties = {\n left: arrowX,\n top: arrowY,\n right: '',\n bottom: '',\n [staticSide]: '-4px',\n };\n\n return {\n ...floatingProps,\n reference: mergedReferenceRef,\n arrowRef,\n arrowStyle,\n };\n}\n","import {useEffect, useState} from 'react';\n\nexport interface UseMediaQueryOptions {\n noSSR?: boolean;\n}\n\nexport function useMediaQuery(\n query: string,\n {noSSR}: UseMediaQueryOptions = {noSSR: true}\n) {\n const supportsMatchMedia =\n typeof window !== 'undefined' && typeof window.matchMedia === 'function';\n const [matches, setMatches] = useState(\n noSSR\n ? () => (supportsMatchMedia ? window.matchMedia(query).matches : false)\n : null\n );\n\n useEffect(() => {\n if (!supportsMatchMedia) {\n return;\n }\n\n const mq = window.matchMedia(query);\n const onChange = () => {\n setMatches(mq.matches);\n };\n\n mq.addEventListener('change', onChange);\n if (!noSSR) {\n onChange();\n }\n\n return () => {\n mq.removeEventListener('change', onChange);\n };\n }, [supportsMatchMedia, query, noSSR]);\n\n // If in SSR, the media query should never match. Once the page hydrates,\n // this will update and the real value will be returned.\n return typeof window === 'undefined' ? null : matches;\n}\n","import { useMediaQuery, UseMediaQueryOptions } from \"./use-media-query\";\n\nexport function useIsMobileMediaQuery(options?: UseMediaQueryOptions) {\n return useMediaQuery(\"(max-width: 768px)\", options);\n}\n","import {HTMLMotionProps} from 'framer-motion';\n\nexport const PopoverAnimation: HTMLMotionProps<'div'> = {\n initial: {opacity: 0, y: 5},\n animate: {opacity: 1, y: 0},\n exit: {opacity: 0, y: 5},\n transition: {type: 'tween', duration: 0.125},\n};\n","import {useViewportSize} from '@react-aria/utils';\n\nexport function useOverlayViewport(): Record {\n const {width, height} = useViewportSize();\n return {\n '--be-viewport-height': `${height}px`,\n '--be-viewport-width': `${width}px`,\n };\n}\n","import React, {\n forwardRef,\n RefObject,\n useCallback,\n useEffect,\n useRef,\n} from 'react';\nimport {m} from 'framer-motion';\nimport {mergeProps, useObjectRef} from '@react-aria/utils';\nimport {PopoverAnimation} from './popover-animation';\nimport {OverlayProps} from './overlay-props';\nimport {useOverlayViewport} from './use-overlay-viewport';\nimport {FocusScope} from '@react-aria/focus';\nimport {VirtualElement} from '@floating-ui/react-dom';\n\nexport const Popover = forwardRef(\n (\n {\n children,\n style,\n autoFocus = false,\n restoreFocus = true,\n isDismissable,\n isContextMenu,\n isOpen,\n onClose,\n triggerRef,\n arrowRef,\n arrowStyle,\n onPointerLeave,\n onPointerEnter,\n },\n ref\n ) => {\n const viewPortStyle = useOverlayViewport();\n const objRef = useObjectRef(ref);\n\n const {domProps} = useCloseOnInteractOutside(\n {\n isDismissable,\n isOpen,\n onClose,\n triggerRef,\n isContextMenu,\n },\n objRef\n );\n\n return (\n \n \n {children}\n \n \n );\n }\n);\n\n// this should only be rendered when overlay is open\nconst visibleOverlays: RefObject[] = [];\ninterface useCloseOnInteractOutsideProps {\n isOpen: boolean;\n onClose: () => void;\n isDismissable: boolean;\n isContextMenu?: boolean;\n triggerRef: OverlayProps['triggerRef'];\n}\nfunction useCloseOnInteractOutside(\n {\n onClose,\n isDismissable = true,\n triggerRef,\n isContextMenu = false,\n }: useCloseOnInteractOutsideProps,\n ref: RefObject\n) {\n const stateRef = useRef({\n isPointerDown: false,\n isContextMenu,\n onClose,\n });\n const state = stateRef.current;\n state.isContextMenu = isContextMenu;\n state.onClose = onClose;\n\n const isValidEvent = useCallback(\n (e: PointerEvent | MouseEvent) => {\n // if (e.button > 0 && (!state.isContextMenu || e.button !== 2)) {\n // return false;\n // }\n\n const target = e.target as Element;\n\n // if the event target is no longer in the document\n if (target) {\n const ownerDocument = target.ownerDocument;\n if (!ownerDocument || !ownerDocument.documentElement.contains(target)) {\n return false;\n }\n }\n\n return ref.current && !ref.current.contains(target);\n },\n [ref]\n );\n\n // Only hide the overlay when it is the topmost visible overlay in the stack.\n // For context menu, hide it regardless\n const isTopMostPopover = useCallback(() => {\n return visibleOverlays[visibleOverlays.length - 1] === ref;\n }, [ref]);\n\n const hideOverlay = useCallback(() => {\n if (isTopMostPopover()) {\n state.onClose();\n }\n }, [isTopMostPopover, state]);\n\n const clickedOnTriggerElement = useCallback(\n (el: Element) => {\n if (triggerRef.current && 'contains' in triggerRef.current) {\n return triggerRef.current.contains?.(el);\n }\n return false;\n },\n [triggerRef]\n );\n\n const onInteractOutsideStart = useCallback(\n (e: PointerEvent) => {\n if (!clickedOnTriggerElement(e.target as Element)) {\n if (isTopMostPopover()) {\n e.stopPropagation();\n e.preventDefault();\n }\n }\n },\n [clickedOnTriggerElement, isTopMostPopover]\n );\n\n const onInteractOutside = useCallback(\n (e: PointerEvent) => {\n if (!clickedOnTriggerElement(e.target as Element)) {\n if (isTopMostPopover()) {\n e.stopPropagation();\n e.preventDefault();\n }\n // don't close context menu on right click, it will be done in \"onInteractOutsideStart\" already.\n // And it would prevent repositioning of context menu when right-clicking on the same element\n if (!state.isContextMenu || e.button !== 2) {\n hideOverlay();\n }\n }\n },\n [clickedOnTriggerElement, hideOverlay, state, isTopMostPopover]\n );\n\n // Add popover ref to the stack of visible popovers on mount, and remove on unmount.\n useEffect(() => {\n visibleOverlays.push(ref);\n\n // handle pointer up and down events\n const onPointerDown = (e: PointerEvent) => {\n if (isValidEvent(e)) {\n onInteractOutsideStart(e);\n stateRef.current.isPointerDown = true;\n }\n };\n const onPointerUp = (e: PointerEvent) => {\n if (stateRef.current.isPointerDown && isValidEvent(e)) {\n stateRef.current.isPointerDown = false;\n onInteractOutside(e);\n }\n };\n\n // handle context menu event\n const onContextMenu = (e: MouseEvent) => {\n e.preventDefault();\n if (isValidEvent(e)) {\n hideOverlay();\n }\n };\n\n // handle closing on scroll\n const onScroll = (e: Event) => {\n if (!triggerRef.current) {\n return;\n }\n\n const scrollableRegion = e.target;\n let triggerEl: Element | undefined;\n if (triggerRef.current instanceof Node) {\n triggerEl = triggerRef.current;\n } else if ('contextElement' in triggerRef.current) {\n triggerEl = (triggerRef.current as VirtualElement).contextElement;\n }\n // window is not a Node and doesn't have \"contain\", but window contains everything\n if (\n !(scrollableRegion instanceof Node) ||\n !triggerEl ||\n scrollableRegion.contains(triggerEl)\n ) {\n state.onClose();\n }\n };\n\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('pointerup', onPointerUp, true);\n document.addEventListener('contextmenu', onContextMenu, true);\n document.addEventListener('scroll', onScroll, true);\n\n return () => {\n const index = visibleOverlays.indexOf(ref);\n if (index >= 0) {\n visibleOverlays.splice(index, 1);\n }\n document.removeEventListener('pointerdown', onPointerDown, true);\n document.removeEventListener('pointerup', onPointerUp, true);\n document.removeEventListener('contextmenu', onContextMenu, true);\n document.removeEventListener('scroll', onScroll, true);\n };\n }, [\n ref,\n isValidEvent,\n state,\n onInteractOutside,\n onInteractOutsideStart,\n triggerRef,\n clickedOnTriggerElement,\n hideOverlay,\n ]);\n\n // Handle the escape key\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.stopPropagation();\n e.preventDefault();\n hideOverlay();\n }\n };\n\n return {\n domProps: {\n onKeyDown,\n },\n };\n}\n","import {HTMLMotionProps} from 'framer-motion';\n\nexport const opacityAnimation: HTMLMotionProps = {\n initial: {opacity: 0},\n animate: {opacity: 1},\n exit: {opacity: 0},\n transition: {duration: 0.2},\n};\n","import {m} from 'framer-motion';\nimport clsx from 'clsx';\nimport {ComponentPropsWithoutRef} from 'react';\nimport {opacityAnimation} from '../animation/opacity-animation';\n\ninterface UnderlayProps\n extends Omit<\n ComponentPropsWithoutRef<'div'>,\n 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag'\n > {\n position?: 'fixed' | 'absolute';\n className?: string;\n isTransparent?: boolean;\n disableInitialTransition?: boolean;\n}\nexport function Underlay({\n position = 'absolute',\n className,\n isTransparent = false,\n disableInitialTransition,\n ...domProps\n}: UnderlayProps) {\n return (\n \n );\n}\n","import {m} from 'framer-motion';\nimport {forwardRef} from 'react';\nimport {OverlayProps} from './overlay-props';\nimport {useOverlayViewport} from './use-overlay-viewport';\nimport {Underlay} from './underlay';\nimport {FocusScope} from '@react-aria/focus';\nimport {useObjectRef} from '@react-aria/utils';\n\nexport const Tray = forwardRef(\n (\n {\n children,\n autoFocus = false,\n restoreFocus = true,\n isDismissable,\n isOpen,\n onClose,\n },\n ref\n ) => {\n const viewPortStyle = useOverlayViewport();\n const objRef = useObjectRef(ref);\n\n return (\n
\n {\n if (isDismissable) {\n onClose();\n }\n }}\n />\n \n \n {children}\n \n \n
\n );\n }\n);\n","import {forwardRef} from 'react';\nimport {m} from 'framer-motion';\nimport {OverlayProps} from './overlay-props';\nimport {useOverlayViewport} from './use-overlay-viewport';\nimport {Underlay} from './underlay';\nimport {FocusScope} from '@react-aria/focus';\nimport {useObjectRef} from '@react-aria/utils';\nimport clsx from 'clsx';\n\nexport const Modal = forwardRef(\n (\n {\n children,\n autoFocus = false,\n restoreFocus = true,\n isDismissable = true,\n isOpen = false,\n placement = 'center',\n onClose,\n },\n ref\n ) => {\n const viewPortStyle = useOverlayViewport();\n const objRef = useObjectRef(ref);\n\n return (\n {\n if (e.key === 'Escape') {\n e.stopPropagation();\n e.preventDefault();\n onClose();\n }\n }}\n >\n {\n if (isDismissable) {\n onClose();\n }\n }}\n />\n \n \n {children}\n \n \n \n );\n }\n);\n","import React, {ReactNode, useId} from 'react';\nimport clsx from 'clsx';\n\nexport interface ListboxSectionProps {\n label?: ReactNode;\n children: React.ReactNode;\n index?: number;\n}\nexport function Section({children, label, index}: ListboxSectionProps) {\n const id = useId();\n\n return (\n \n {label && (\n \n {label}\n \n )}\n {children}\n \n );\n}\n","import {Children, isValidElement, ReactElement, ReactNode} from 'react';\nimport memoize from 'nano-memoize';\nimport {ListboxItemProps} from './item';\nimport {ListboxSectionProps, Section} from './section';\nimport {ListBoxChildren} from './types';\nimport {MessageDescriptor} from '@common/i18n/message-descriptor';\n\nexport type ListboxCollection = Map>;\n\nexport type CollectionItem = {\n index: number;\n textLabel: string;\n element: ReactElement;\n value: string | number;\n item?: T;\n isDisabled?: boolean;\n section?: ReactElement;\n};\n\ntype Props = ListBoxChildren & {\n inputValue?: string;\n maxItems?: number;\n};\n\nexport const buildListboxCollection = memoize(\n ({maxItems, children, items, inputValue}: Props) => {\n let collection = childrenToCollection({children, items});\n let filteredCollection = filterCollection({collection, inputValue});\n\n if (maxItems) {\n collection = new Map([...collection.entries()].slice(0, maxItems));\n filteredCollection = new Map(\n [...filteredCollection.entries()].slice(0, maxItems)\n );\n }\n\n return {collection, filteredCollection};\n }\n);\n\ntype filterCollectionProps = {\n collection: ListboxCollection;\n inputValue?: string;\n};\nconst filterCollection = memoize(\n ({collection, inputValue}: filterCollectionProps) => {\n let filteredCollection: ListboxCollection = new Map();\n\n const query = inputValue ? `${inputValue}`.toLowerCase().trim() : '';\n if (!query) {\n filteredCollection = collection;\n } else {\n let filterIndex = 0;\n collection.forEach((meta, value) => {\n const haystack = meta.item ? JSON.stringify(meta.item) : meta.textLabel;\n if (haystack.toLowerCase().trim().includes(query)) {\n filteredCollection.set(value, {...meta, index: filterIndex++});\n }\n });\n }\n\n return filteredCollection;\n }\n);\n\nconst childrenToCollection = memoize(\n ({children, items}: ListBoxChildren) => {\n let reactChildren: ReactNode;\n if (items && typeof children === 'function') {\n reactChildren = items.map(item => children(item));\n } else {\n reactChildren = children as ReactNode;\n }\n\n const collection = new Map>();\n let optionIndex = 0;\n\n const setOption = (\n element: ReactElement,\n section?: any,\n sectionIndex?: number,\n sectionItemIndex?: number\n ) => {\n const index = optionIndex++;\n const item = section\n ? // get item from nested array\n items?.[sectionIndex!].items[sectionItemIndex!]\n : // get item from flat array\n items?.[index];\n\n collection.set(element.props.value, {\n index,\n element,\n textLabel: getTextLabel(element),\n item,\n section,\n isDisabled: element.props.isDisabled,\n value: element.props.value,\n });\n };\n\n Children.forEach(reactChildren, (child, childIndex) => {\n if (!isValidElement(child)) return;\n if (child.type === Section) {\n Children.forEach(\n child.props.children,\n (nestedChild, nestedChildIndex) => {\n setOption(nestedChild, child, childIndex, nestedChildIndex);\n }\n );\n } else {\n setOption(child as ReactElement);\n }\n });\n\n return collection;\n }\n);\n\nfunction getTextLabel(item: ReactElement): string {\n const content = item.props.children as any;\n\n if (item.props.textLabel) {\n return item.props.textLabel;\n }\n if ((content?.props as MessageDescriptor)?.message) {\n return content.props.message;\n }\n\n return `${content}` || '';\n}\n","import React, {Ref, useCallback, useId, useMemo, useRef, useState} from 'react';\nimport {useControlledState} from '@react-stately/utils';\nimport {\n buildListboxCollection,\n CollectionItem,\n} from './build-listbox-collection';\nimport {useFloatingPosition} from '../../overlays/floating-position';\nimport {\n ListBoxChildren,\n ListboxProps,\n PrimitiveValue,\n UseListboxReturn,\n} from './types';\nimport {VirtualElement} from '@floating-ui/react-dom';\n\nexport function useListbox(\n props: ListboxProps & ListBoxChildren,\n ref?: Ref,\n): UseListboxReturn {\n const {\n children,\n items,\n role = 'listbox',\n virtualFocus,\n loopFocus = false,\n onItemSelected,\n clearInputOnItemSelection,\n blurReferenceOnItemSelection,\n floatingWidth = 'matchTrigger',\n floatingMinWidth,\n floatingMaxHeight,\n offset,\n placement,\n showCheckmark,\n showEmptyMessage,\n maxItems,\n isAsync,\n allowCustomValue,\n clearSelectionOnInputClear,\n } = props;\n const selectionMode = props.selectionMode || 'none';\n const id = useId();\n const listboxId = `${id}-listbox`;\n\n // controlled state for text input (if in combobox mode)\n const [inputValue, setInputValue] = useControlledState(\n props.inputValue,\n props.defaultInputValue || '',\n props.onInputValueChange,\n );\n\n // mostly for combobox, so can show all collection items on dropdown icon click, even if user has filtered via input\n const [activeCollection, setActiveCollection] = useState<'all' | 'filtered'>(\n 'all',\n );\n\n const collections = buildListboxCollection({\n children,\n items,\n // don't filter on client side if async, it will already be filtered on server\n inputValue: isAsync ? undefined : inputValue,\n maxItems,\n });\n const collection =\n activeCollection === 'all'\n ? collections.collection\n : collections.filteredCollection;\n\n // items for keyboard navigation\n const listItemsRef = useRef>([]);\n\n // plain text labels for typeahead\n const listContent: (string | null)[] = useMemo(() => {\n return [...collection.values()].map(o =>\n o.isDisabled ? null : o.textLabel,\n );\n }, [collection]);\n\n // state for currently selected values (always array, even in single selection mode)\n const {selectedValues, selectValues} = useControlledSelection(props);\n\n const [isOpen, setIsOpen] = useControlledState(\n props.isOpen,\n props.defaultIsOpen,\n props.onOpenChange,\n );\n const [activeIndex, setActiveIndex] = useState(null);\n\n // handle listbox positioning relative to trigger\n const floatingProps = useFloatingPosition({\n floatingWidth,\n ref,\n placement,\n offset,\n maxHeight: floatingMaxHeight ?? 420,\n // don't shift floating menu on the sides of combobox, otherwise input might get obscured\n shiftCrossAxis: !virtualFocus,\n });\n const {refs, strategy, x, y} = floatingProps;\n\n // handle selection state for syncing with active index in keyboard navigation\n const selectedOption =\n selectionMode === 'none' ? undefined : collection.get(selectedValues[0]);\n const selectedIndex =\n selectionMode === 'none' ? undefined : selectedOption?.index;\n const setSelectedIndex = (index: number) => {\n if (selectionMode !== 'none') {\n const item = [...collection.values()][index];\n if (item) {\n selectValues(item.value);\n }\n }\n };\n\n // focus and scroll to specified index, in both virtual and regular mode.\n // will also skip disabled indices and focus next or previous non-disabled index instead\n const focusItem = useCallback(\n (fallbackOperation: 'increment' | 'decrement', newIndex: number) => {\n const items = [...collection.values()];\n const allItemsDisabled = !items.find(i => !i.isDisabled);\n const lastIndex = collection.size - 1;\n\n // invalid index\n if (\n newIndex == null ||\n !collection.size ||\n newIndex > lastIndex ||\n newIndex < 0 ||\n allItemsDisabled\n ) {\n setActiveIndex(null);\n return;\n }\n\n // get next or previous non-disabled item\n newIndex = getNonDisabledIndex(\n items,\n newIndex,\n loopFocus,\n fallbackOperation,\n );\n\n setActiveIndex(newIndex);\n\n if (virtualFocus) {\n listItemsRef.current[newIndex]?.scrollIntoView({\n block: 'nearest',\n });\n } else {\n listItemsRef.current[newIndex]?.focus();\n }\n },\n [collection, virtualFocus, loopFocus],\n );\n\n const onInputChange = useCallback(\n (e: React.ChangeEvent) => {\n setInputValue(e.target.value);\n\n setActiveCollection(e.target.value.trim() ? 'filtered' : 'all');\n\n if (e.target.value) {\n setIsOpen(true);\n } else if (clearSelectionOnInputClear) {\n // deselect currently selected option if user fully clears the input\n selectValues('');\n }\n\n focusItem('increment', 0);\n },\n [\n setInputValue,\n setIsOpen,\n setActiveCollection,\n selectValues,\n isAsync,\n clearSelectionOnInputClear,\n focusItem,\n ],\n );\n\n const handleItemSelection = (value: PrimitiveValue) => {\n const reference = refs.reference.current as\n | HTMLElement\n | VirtualElement\n | null;\n if (selectionMode !== 'none') {\n selectValues(value);\n } else {\n if (reference && 'focus' in reference) {\n reference.focus();\n }\n }\n // is combobox\n if (virtualFocus) {\n setInputValue(clearInputOnItemSelection ? '' : `${value}`);\n if (blurReferenceOnItemSelection && reference && 'blur' in reference) {\n reference.blur();\n }\n }\n setActiveCollection('all');\n setIsOpen(false);\n onItemSelected?.(value);\n // make sure \"onItemSelected\" callback has a chance to use activeIndex value, before clearing it\n setActiveIndex(null);\n };\n\n return {\n // even handlers\n handleItemSelection,\n onInputChange,\n loopFocus,\n\n // config\n floatingWidth,\n floatingMinWidth,\n floatingMaxHeight,\n showCheckmark,\n collection,\n collections,\n virtualFocus,\n focusItem,\n showEmptyMessage: showEmptyMessage && !!inputValue,\n allowCustomValue,\n\n // floating ui\n refs,\n reference: floatingProps.reference,\n floating: refs.setFloating,\n positionStyle: {\n position: strategy,\n top: y ?? '',\n left: x ?? '',\n },\n\n listContent,\n listItemsRef,\n listboxId,\n role,\n\n state: {\n // currently focused or active (if virtual focus) option\n activeIndex,\n setActiveIndex,\n selectedIndex,\n setSelectedIndex,\n selectionMode,\n selectedValues,\n selectValues,\n inputValue,\n setInputValue,\n isOpen,\n setIsOpen,\n setActiveCollection,\n },\n };\n}\n\nfunction getNonDisabledIndex(\n items: CollectionItem[],\n newIndex: number,\n loopFocus: boolean,\n operation: 'increment' | 'decrement',\n) {\n const lastIndex = items.length - 1;\n while (items[newIndex]?.isDisabled) {\n if (operation === 'increment') {\n newIndex++;\n if (newIndex >= lastIndex) {\n // loop from the start, if end reached\n if (loopFocus) {\n newIndex = 0;\n // if focus is not looping, stay on the previous index\n } else {\n return newIndex - 1;\n }\n }\n } else {\n newIndex--;\n // loop from the end, if start reached\n if (newIndex < 0) {\n if (loopFocus) {\n newIndex = lastIndex;\n // if focus is not looping, stay on the previous index\n } else {\n return newIndex + 1;\n }\n }\n }\n }\n\n return newIndex;\n}\n\nfunction useControlledSelection(props: ListboxProps) {\n const {selectionMode, allowEmptySelection} = props;\n const selectionEnabled =\n selectionMode === 'single' || selectionMode === 'multiple';\n\n const [stateValues, setStateValues] = useControlledState(\n !selectionEnabled ? undefined : props.selectedValue,\n !selectionEnabled ? undefined : props.defaultSelectedValue,\n !selectionEnabled ? undefined : props.onSelectionChange,\n );\n\n const selectedValues = useMemo(() => {\n if (stateValues == null) {\n return [];\n }\n return Array.isArray(stateValues) ? stateValues : [stateValues];\n }, [stateValues]);\n\n const selectValues = useCallback(\n (mixedValue: PrimitiveValue | PrimitiveValue[] | null) => {\n const newValues = Array.isArray(mixedValue) ? mixedValue : [mixedValue];\n if (selectionMode === 'single') {\n setStateValues(newValues[0]);\n } else {\n newValues.forEach(newValue => {\n const index = selectedValues.indexOf(newValue);\n if (index === -1) {\n selectedValues.push(newValue);\n setStateValues([...selectedValues]);\n } else if (selectedValues.length > 1 || allowEmptySelection) {\n selectedValues.splice(index, 1);\n setStateValues([...selectedValues]);\n }\n });\n }\n },\n [allowEmptySelection, selectedValues, selectionMode, setStateValues],\n );\n\n return {\n selectedValues,\n selectValues,\n };\n}\n","import {createContext, useContext} from 'react';\nimport {UseListboxReturn} from './types';\n\ntype ListBoxReturnType = UseListboxReturn;\nexport type ListboxContextValue = ListBoxReturnType;\n\nexport const ListBoxContext = createContext(null!);\n\nexport function useListboxContext() {\n return useContext(ListBoxContext);\n}\n","import {useIsSSR} from '@react-aria/ssr';\nimport {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';\n\nconst MOBILE_SCREEN_WIDTH = 768;\n\nexport function useIsMobileDevice(): boolean {\n const isSSR = useIsSSR();\n if (isSSR || typeof window === 'undefined') {\n return getBootstrapData().is_mobile_device;\n }\n\n return window.screen.width <= MOBILE_SCREEN_WIDTH;\n}\n","import {AnimatePresence} from 'framer-motion';\nimport React, {\n cloneElement,\n ComponentPropsWithoutRef,\n JSXElementConstructor,\n ReactElement,\n ReactNode,\n RefObject,\n useEffect,\n useMemo,\n useRef,\n} from 'react';\nimport clsx from 'clsx';\nimport {ListBoxContext, useListboxContext} from './listbox-context';\nimport {useIsMobileDevice} from '@common/utils/hooks/is-mobile-device';\nimport {Popover} from '../../overlays/popover';\nimport {Tray} from '../../overlays/tray';\nimport {Trans} from '@common/i18n/trans';\nimport {createPortal} from 'react-dom';\nimport {UseListboxReturn} from './types';\nimport {OverlayProps} from '../../overlays/overlay-props';\nimport {rootEl} from '@common/core/root-el';\n\ninterface Props extends ComponentPropsWithoutRef<'div'> {\n listbox: UseListboxReturn;\n mobileOverlay?: JSXElementConstructor;\n children?: ReactElement;\n searchField?: ReactNode;\n isLoading?: boolean;\n onClose?: () => void;\n prepend?: boolean;\n}\nexport function Listbox({\n listbox,\n children: trigger,\n isLoading,\n mobileOverlay = Tray,\n searchField,\n onClose,\n prepend,\n className: listboxClassName,\n ...domProps\n}: Props) {\n const isMobile = useIsMobileDevice();\n const {\n floatingWidth,\n floatingMinWidth = 'min-w-180',\n collection,\n showEmptyMessage,\n state: {isOpen, setIsOpen},\n positionStyle,\n floating,\n refs,\n } = listbox;\n\n const Overlay = !prepend && isMobile ? mobileOverlay : Popover;\n\n const className = clsx(\n 'text-base sm:text-sm outline-none bg-paper max-h-inherit flex flex-col',\n !prepend && 'shadow-xl border py-4',\n listboxClassName,\n\n // tray will apply its own rounding and max width\n Overlay === Popover && 'rounded',\n Overlay === Popover && floatingWidth === 'auto'\n ? `max-w-288 ${floatingMinWidth}`\n : '',\n );\n\n const children = useMemo(() => {\n let sectionIndex = 0;\n const renderedSections: ReactElement[] = [];\n return [...collection.values()].reduce((prev, curr) => {\n if (!curr.section) {\n prev.push(\n cloneElement(curr.element, {\n key: curr.element.key || curr.element.props.value,\n }),\n );\n } else if (!renderedSections.includes(curr.section)) {\n const section = cloneElement(curr.section, {\n key: curr.section.key || sectionIndex,\n index: sectionIndex,\n });\n prev.push(section);\n // clone element will create new instance of object, need to keep\n // track of original instance so sections are not duplicated\n renderedSections.push(curr.section);\n sectionIndex++;\n }\n return prev;\n }, []);\n }, [collection]);\n\n const showContent = children.length > 0 || (showEmptyMessage && !isLoading);\n\n const innerContent = showContent ? (\n
\n {searchField}\n \n {children}\n \n
\n ) : null;\n\n return (\n \n {trigger}\n {prepend\n ? innerContent\n : rootEl &&\n createPortal(\n \n {isOpen && showContent && (\n }\n restoreFocus\n isOpen={isOpen}\n onClose={() => {\n onClose?.();\n setIsOpen(false);\n }}\n isDismissable\n style={positionStyle}\n ref={floating}\n >\n {innerContent!}\n \n )}\n ,\n rootEl,\n )}\n \n );\n}\n\ninterface WrapperProps extends ComponentPropsWithoutRef<'div'> {\n isLoading?: boolean;\n children: ReactElement[];\n}\nfunction FocusContainer({\n className,\n children,\n isLoading,\n ...domProps\n}: WrapperProps) {\n const {\n role,\n listboxId,\n virtualFocus,\n focusItem,\n state: {activeIndex, setActiveIndex, selectedIndex},\n } = useListboxContext();\n const autoFocusRef = useRef(true);\n const domRef = useRef(null);\n\n // reset activeIndex on unmount\n useEffect(() => {\n return () => setActiveIndex(null);\n }, [setActiveIndex]);\n\n // focus active index or menu on mount, because menu will be closed\n // on trigger keyDown and focus won't be applied to items\n useEffect(() => {\n if (autoFocusRef.current) {\n const indexToFocus = activeIndex ?? selectedIndex;\n // if no activeIndex, focus menu itself\n if (indexToFocus == null && !virtualFocus) {\n requestAnimationFrame(() => {\n domRef.current?.focus({preventScroll: true});\n });\n } else if (indexToFocus != null) {\n // wait until next frame, otherwise auto scroll might not work\n requestAnimationFrame(() => {\n focusItem('increment', indexToFocus);\n });\n }\n }\n autoFocusRef.current = false;\n }, [activeIndex, selectedIndex, focusItem, virtualFocus]);\n\n return (\n \n {children.length ? children : }\n \n );\n}\n\nfunction EmptyMessage() {\n return (\n
\n \n
\n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const CheckIcon = createSvgIcon(\n \n, 'CheckOutlined');\n","import clsx from 'clsx';\nimport React, {\n ComponentPropsWithRef,\n JSXElementConstructor,\n ReactNode,\n} from 'react';\nimport {CheckIcon} from '../../icons/material/Check';\nimport {To} from 'react-router-dom';\n\nexport interface ListItemBaseProps extends ComponentPropsWithRef<'div'> {\n startIcon?: ReactNode;\n endIcon?: ReactNode;\n endSection?: ReactNode;\n description?: ReactNode;\n textLabel?: string;\n capitalizeFirst?: boolean;\n isSelected?: boolean;\n isDisabled?: boolean;\n isActive?: boolean;\n className?: string;\n showCheckmark?: boolean;\n elementType?: 'a' | JSXElementConstructor | 'div';\n to?: To;\n href?: string;\n radius?: string;\n padding?: string;\n}\n\nexport const ListItemBase = React.forwardRef(\n (props, ref) => {\n let {\n startIcon,\n capitalizeFirst,\n children,\n description,\n endIcon,\n endSection,\n isDisabled,\n isActive,\n isSelected,\n showCheckmark,\n elementType = 'div',\n radius,\n padding,\n ...domProps\n } = props;\n\n if (!startIcon && showCheckmark) {\n startIcon = (\n \n );\n }\n\n // if (!endIcon && !endSection && showCheckmark) {\n // endIcon = (\n // \n // );\n // }\n\n const iconClassName = clsx(\n 'icon-sm rounded overflow-hidden flex-shrink-0',\n !isDisabled && 'text-muted'\n );\n const endSectionClassName = clsx(!isDisabled && 'text-muted');\n\n const Element = elementType;\n\n return (\n \n {startIcon &&
{startIcon}
}\n \n {children}\n {description && (\n \n {description}\n \n )}\n \n {(endIcon || endSection) && (\n
\n {endIcon || endSection}\n
\n )}\n
\n );\n }\n);\n\nfunction itemClassName({\n className,\n isSelected,\n isActive,\n isDisabled,\n showCheckmark,\n endIcon,\n endSection,\n radius,\n padding: userPadding,\n}: ListItemBaseProps): string {\n let state: string = '';\n if (isDisabled) {\n state = 'text-disabled pointer-events-none';\n } else if (isSelected) {\n if (isActive) {\n state = 'bg-primary/focus';\n } else {\n state = 'bg-primary/selected hover:bg-primary/focus';\n }\n } else if (isActive) {\n state = 'hover:bg-fg-base/15 bg-focus';\n } else {\n state = 'hover:bg-hover';\n }\n\n let padding;\n\n if (userPadding) {\n padding = userPadding;\n } else if (showCheckmark) {\n if (endIcon || endSection) {\n padding = 'pl-8 pr-8';\n } else {\n padding = 'pl-8 pr-24';\n }\n } else {\n padding = 'px-20';\n }\n\n return clsx(\n 'w-full select-none outline-none cursor-pointer',\n 'py-8 text-sm truncate flex items-center gap-10',\n !isDisabled && 'text-main',\n padding,\n state,\n className,\n radius\n );\n}\n","import React from 'react';\nimport {useListboxContext} from './listbox-context';\nimport {ListItemBase, ListItemBaseProps} from '../../list/list-item-base';\n\nexport interface ListboxItemProps extends ListItemBaseProps {\n value: any;\n textLabel?: string;\n onSelected?: () => void;\n onKeyDown?: any;\n tabIndex?: number;\n className?: string;\n capitalizeFirst?: boolean;\n}\nexport function Item({\n children,\n value,\n startIcon,\n endIcon,\n endSection,\n description,\n capitalizeFirst,\n textLabel,\n isDisabled,\n onSelected,\n onClick,\n ...domProps\n}: ListboxItemProps) {\n const {\n collection,\n showCheckmark,\n virtualFocus,\n listboxId,\n role,\n listItemsRef,\n handleItemSelection,\n state: {selectedValues, activeIndex, setActiveIndex},\n } = useListboxContext();\n const isSelected = selectedValues.includes(value);\n const index = collection.get(value)?.index;\n const isActive = activeIndex === index;\n\n // context value might get out of sync with item due to AnimatePresence\n if (index == null) {\n return null;\n }\n\n const tabIndex = isActive && !isDisabled ? -1 : 0;\n\n return (\n {\n if (!virtualFocus) {\n setActiveIndex(index);\n }\n }}\n onPointerEnter={e => {\n setActiveIndex(index);\n if (!virtualFocus) {\n e.currentTarget.focus();\n }\n }}\n onPointerDown={e => {\n if (virtualFocus) {\n e.preventDefault();\n }\n }}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleItemSelection(value);\n onSelected?.();\n }\n }}\n onClick={e => {\n handleItemSelection(value);\n onSelected?.();\n onClick?.(e);\n }}\n ref={node => (listItemsRef.current[index] = node)}\n id={`${listboxId}-${index}`}\n role={role === 'menu' ? 'menuitem' : 'option'}\n tabIndex={virtualFocus ? undefined : tabIndex}\n aria-selected={isActive && isSelected}\n showCheckmark={showCheckmark}\n isDisabled={isDisabled}\n isActive={isActive}\n isSelected={isSelected}\n startIcon={startIcon}\n description={description}\n endIcon={endIcon}\n endSection={endSection}\n capitalizeFirst={capitalizeFirst}\n data-value={value}\n >\n {children}\n \n );\n}\n","import React, {KeyboardEvent} from 'react';\nimport {UseListboxReturn} from './types';\n\nexport function useListboxKeyboardNavigation({\n state: {isOpen, setIsOpen, selectedIndex, activeIndex, setInputValue},\n loopFocus,\n collection,\n focusItem,\n handleItemSelection,\n allowCustomValue,\n}: UseListboxReturn) {\n const handleTriggerKeyDown = (e: React.KeyboardEvent): true | void => {\n // ignore if dropdown is open or if event bubbled up from portal\n if (isOpen || !e.currentTarget.contains(e.target as HTMLElement)) return;\n\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n setIsOpen(true);\n focusItem('increment', selectedIndex != null ? selectedIndex : 0);\n return true;\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n setIsOpen(true);\n focusItem(\n 'decrement',\n selectedIndex != null ? selectedIndex : collection.size - 1\n );\n return true;\n } else if (e.key === 'Enter' || e.key === 'Space') {\n e.preventDefault();\n setIsOpen(true);\n focusItem('increment', selectedIndex != null ? selectedIndex : 0);\n return true;\n }\n };\n\n const handleListboxKeyboardNavigation = (\n e: React.KeyboardEvent\n ): true | void => {\n const lastIndex = Math.max(0, collection.size - 1);\n // ignore if event bubbled up from portal, or dropdown is closed\n if (!isOpen || !e.currentTarget.contains(e.target as HTMLElement)) return;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n if (activeIndex == null) {\n focusItem('increment', 0);\n } else if (activeIndex >= lastIndex) {\n // if focus is not looping, stay on last index\n if (loopFocus) {\n focusItem('increment', 0);\n }\n } else {\n focusItem('increment', activeIndex + 1);\n }\n return true;\n case 'ArrowUp':\n e.preventDefault();\n if (activeIndex == null) {\n focusItem('decrement', lastIndex);\n } else if (activeIndex <= 0) {\n // if focus is not looping, stay on first index\n if (loopFocus) {\n focusItem('decrement', lastIndex);\n }\n } else {\n focusItem('decrement', activeIndex - 1);\n }\n return true;\n case 'Home':\n e.preventDefault();\n focusItem('increment', 0);\n return true;\n case 'End':\n e.preventDefault();\n focusItem('decrement', lastIndex);\n return true;\n case 'Tab':\n setIsOpen(false);\n return true;\n }\n };\n\n const handleListboxSearchFieldKeydown = (\n e: KeyboardEvent\n ) => {\n if (e.key === 'Enter' && activeIndex != null && collection.size) {\n // prevent form submit when selecting item in combobox via \"enter\"\n e.preventDefault();\n const [value, obj] = [...collection.entries()][activeIndex];\n if (value) {\n handleItemSelection(value);\n // \"onSelected\" will not be called for dropdown items, because keydown\n // event will never be triggered for them in \"virtualFocus\" mode\n obj.element.props.onSelected?.();\n }\n return;\n }\n\n // on escape, clear input and close dropdown\n if (e.key === 'Escape' && isOpen) {\n setIsOpen(false);\n if (!allowCustomValue) {\n setInputValue('');\n }\n }\n\n const handled = handleTriggerKeyDown(e);\n if (!handled) {\n handleListboxKeyboardNavigation(e);\n }\n };\n\n return {\n handleTriggerKeyDown,\n handleListboxKeyboardNavigation,\n handleListboxSearchFieldKeydown,\n };\n}\n","import {useSelectedLocale} from './selected-locale';\n\nconst cache = new Map();\n\nexport function useCollator(options?: Intl.CollatorOptions): Intl.Collator {\n const {localeCode} = useSelectedLocale();\n\n const cacheKey =\n localeCode +\n (options\n ? Object.entries(options)\n .sort((a, b) => (a[0] < b[0] ? -1 : 1))\n .join()\n : '');\n\n if (cache.has(cacheKey)) {\n return cache.get(cacheKey)!;\n }\n\n const formatter = new Intl.Collator(localeCode, options);\n cache.set(cacheKey, formatter);\n return formatter;\n}\n","import React, {useRef} from 'react';\nimport {useCollator} from '../../../i18n/use-collator';\n\ninterface UseTypeSelectReturn {\n findMatchingItem: (\n e: React.KeyboardEvent,\n listContent: (string | null)[],\n fromIndex?: number | null\n ) => number | null;\n}\n\ninterface SearchState {\n search: string;\n timeout: ReturnType | undefined;\n}\n\nexport function useTypeSelect(): UseTypeSelectReturn {\n const collator = useCollator({usage: 'search', sensitivity: 'base'});\n const state = useRef({\n search: '',\n timeout: undefined,\n }).current;\n\n const getMatchingIndex = (\n listContent: (string | null)[],\n fromIndex?: number | null\n ) => {\n let index = fromIndex ?? 0;\n while (index != null) {\n const item = listContent[index];\n const substring = item?.slice(0, state.search.length);\n\n if (substring && collator.compare(substring, state.search) === 0) {\n return index;\n }\n\n if (index < listContent.length - 1) {\n index++;\n // reached the end of list\n } else {\n return null;\n }\n }\n\n return null;\n };\n\n const findMatchingItem: UseTypeSelectReturn['findMatchingItem'] = (\n e,\n listContent,\n fromIndex = 0\n ) => {\n const character = getStringForKey(e.key);\n if (!character || e.ctrlKey || e.metaKey) {\n return null;\n }\n\n // Do not propagate the Spacebar event if it's meant to be part of the search.\n // When we time out, the search term becomes empty, hence the check on length.\n // Trimming is to account for the case of pressing the Spacebar more than once,\n // which should cycle through the selection/deselection of the focused item.\n if (character === ' ' && state.search.trim().length > 0) {\n e.preventDefault();\n e.stopPropagation();\n }\n\n state.search += character;\n\n // Use the delegate to find a key to focus.\n // Prioritize items after the currently focused item, falling back to searching the whole list.\n let index = getMatchingIndex(listContent, fromIndex);\n\n // If no key found, search from the top.\n if (index == null) {\n index = getMatchingIndex(listContent, 0);\n }\n\n clearTimeout(state.timeout);\n state.timeout = setTimeout(() => {\n state.search = '';\n }, 500);\n\n return index ?? null;\n };\n\n return {findMatchingItem};\n}\n\nfunction getStringForKey(key: string) {\n // If the key is of length 1, it is an ASCII value.\n // Otherwise, if there are no ASCII characters in the key name,\n // it is a Unicode character.\n // See https://www.w3.org/TR/uievents-key/\n if (key.length === 1 || !/^[A-Z]/i.test(key)) {\n return key;\n }\n\n return '';\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const SearchIcon = createSvgIcon(\n \n, 'SearchOutlined');\n","import clsx from 'clsx';\nimport {BaseFieldProps} from './base-field-props';\nimport {ButtonSize, getButtonSizeStyle} from '../../buttons/button-size';\n\nexport interface InputFieldStyle {\n label: string;\n input: string;\n wrapper: string;\n inputWrapper: string;\n adornment: string;\n append: {size: string; radius: string};\n size: {font: string; height: string};\n description: string;\n error: string;\n}\n\ntype InputFieldStyleProps = Omit<\n BaseFieldProps,\n 'value' | 'defaultValue' | 'onChange'\n>;\n\nexport function getInputFieldClassNames(\n props: InputFieldStyleProps = {}\n): InputFieldStyle {\n const {\n size = 'md',\n startAppend,\n endAppend,\n className,\n labelPosition,\n labelDisplay = 'block',\n inputClassName,\n inputWrapperClassName,\n unstyled,\n invalid,\n disabled,\n background = 'bg-transparent',\n flexibleHeight,\n inputShadow = 'shadow-sm',\n descriptionPosition = 'bottom',\n inputRing,\n inputFontSize,\n } = {...props};\n\n if (unstyled) {\n return {\n label: '',\n input: inputClassName || '',\n wrapper: className || '',\n inputWrapper: inputWrapperClassName || '',\n adornment: '',\n append: {size: '', radius: ''},\n size: {font: '', height: ''},\n description: '',\n error: '',\n };\n }\n\n const sizeClass = inputSizeClass({\n size: props.size,\n flexibleHeight,\n });\n if (inputFontSize) {\n sizeClass.font = inputFontSize;\n }\n\n const isInputGroup = startAppend || endAppend;\n\n const ringColor = invalid\n ? 'focus:ring-danger/focus focus:border-danger/60'\n : 'focus:ring-primary/focus focus:border-primary/60';\n const ringClassName = inputRing || `focus:ring ${ringColor}`;\n\n const radius = getRadius(props);\n\n return {\n label: clsx(\n labelDisplay,\n 'first-letter:capitalize text-left whitespace-nowrap',\n disabled && 'text-disabled',\n sizeClass.font,\n labelPosition === 'side' ? 'mr-16' : 'mb-4'\n ),\n input: clsx(\n 'block text-left relative w-full appearance-none transition-shadow text',\n background,\n\n // radius\n radius.input,\n\n getInputBorder(props),\n !disabled && `${ringClassName} focus:outline-none ${inputShadow}`,\n disabled && 'text-disabled cursor-not-allowed',\n inputClassName,\n sizeClass.font,\n sizeClass.height,\n getInputPadding(props)\n ),\n adornment: iconSizeClass(size),\n append: {\n size: getButtonSizeStyle(size),\n radius: radius.append,\n },\n wrapper: clsx(className, sizeClass.font, {\n 'flex items-center': labelPosition === 'side',\n }),\n inputWrapper: clsx(\n 'isolate relative',\n inputWrapperClassName,\n isInputGroup && 'flex items-stretch'\n ),\n size: sizeClass,\n description: `text-muted ${\n descriptionPosition === 'bottom' ? 'pt-10' : 'pb-10'\n } text-xs`,\n error: 'text-danger pt-10 text-xs',\n };\n}\n\nfunction getInputBorder({\n startAppend,\n endAppend,\n inputBorder,\n invalid,\n}: InputFieldStyleProps) {\n if (inputBorder) return inputBorder;\n\n const isInputGroup = startAppend || endAppend;\n const borderColor = invalid ? 'border-danger' : 'border-divider';\n\n if (!isInputGroup) {\n return `${borderColor} border`;\n }\n if (startAppend) {\n return `${borderColor} border-y border-r`;\n }\n return `${borderColor} border-y border-l`;\n}\n\nfunction getInputPadding({\n startAdornment,\n endAdornment,\n inputRadius,\n}: InputFieldStyleProps) {\n if (inputRadius === 'rounded-full') {\n return clsx(\n startAdornment ? 'pl-54' : 'pl-28',\n endAdornment ? 'pr-54' : 'pr-28'\n );\n }\n return clsx(\n startAdornment ? 'pl-46' : 'pl-12',\n endAdornment ? 'pr-46' : 'pr-12'\n );\n}\n\nfunction getRadius(props: InputFieldStyleProps): {\n input: string;\n append: string;\n} {\n const {startAppend, endAppend, inputRadius} = props;\n const isInputGroup = startAppend || endAppend;\n\n if (inputRadius === 'rounded-full') {\n return {\n input: clsx(\n !isInputGroup && 'rounded-full',\n startAppend && 'rounded-r-full rounded-l-none',\n endAppend && 'rounded-l-full rounded-r-none'\n ),\n append: startAppend ? 'rounded-l-full' : 'rounded-r-full',\n };\n } else if (inputRadius === 'rounded-none') {\n return {\n input: '',\n append: '',\n };\n } else if (inputRadius) {\n return {\n input: inputRadius,\n append: inputRadius,\n };\n }\n return {\n input: clsx(\n !isInputGroup && 'rounded',\n startAppend && 'rounded-r rounded-l-none',\n endAppend && 'rounded-l rounded-r-none'\n ),\n append: startAppend ? 'rounded-l' : 'rounded-r',\n };\n}\n\nfunction inputSizeClass({size, flexibleHeight}: BaseFieldProps) {\n switch (size) {\n case '2xs':\n return {font: 'text-xs', height: flexibleHeight ? 'min-h-24' : 'h-24'};\n case 'xs':\n return {font: 'text-xs', height: flexibleHeight ? 'min-h-30' : 'h-30'};\n case 'sm':\n return {font: 'text-sm', height: flexibleHeight ? 'min-h-36' : 'h-36'};\n case 'lg':\n return {\n font: 'text-md md:text-lg',\n height: flexibleHeight ? 'min-h-50' : 'h-50',\n };\n case 'xl':\n return {font: 'text-xl', height: flexibleHeight ? 'min-h-60' : 'h-60'};\n default:\n return {font: 'text-sm', height: flexibleHeight ? 'min-h-42' : 'h-42'};\n }\n}\n\nfunction iconSizeClass(size?: ButtonSize): string {\n switch (size) {\n case '2xs':\n return 'icon-2xs';\n case 'xs':\n return 'icon-xs';\n case 'sm':\n return 'icon-sm';\n case 'md':\n return 'icon-sm';\n case 'lg':\n return 'icon-lg';\n case 'xl':\n return 'icon-xl';\n default:\n // can't return \"size\" variable here, append in field will not work with it\n return '';\n }\n}\n","import React from 'react';\nimport clsx from 'clsx';\n\ntype AdornmentProps = {\n children: React.ReactNode;\n direction: 'start' | 'end';\n position?: string;\n className?: string;\n};\nexport function Adornment({\n children,\n direction,\n className,\n position = direction === 'start' ? 'left-0' : 'right-0',\n}: AdornmentProps) {\n if (!children) return null;\n return (\n \n {children}\n \n );\n}\n","export function removeEmptyValuesFromObject>(\n obj: T\n): T {\n const copy = {...obj};\n Object.keys(copy).forEach(key => {\n if (copy[key] == null || copy[key] === '') {\n delete copy[key];\n }\n });\n return copy;\n}\n","import React, {ComponentPropsWithoutRef, ReactElement, ReactNode} from 'react';\nimport {Adornment} from './adornment';\nimport {InputFieldStyle} from './get-input-field-class-names';\nimport {BaseFieldProps} from './base-field-props';\nimport {removeEmptyValuesFromObject} from '@common/utils/objects/remove-empty-values-from-object';\n\nexport interface FieldProps extends BaseFieldProps {\n children: ReactNode;\n wrapperProps?: ComponentPropsWithoutRef<'div'>;\n labelProps?: ComponentPropsWithoutRef<'label' | 'span'>;\n descriptionProps?: ComponentPropsWithoutRef<'div'>;\n errorMessageProps?: ComponentPropsWithoutRef<'div'>;\n fieldClassNames: InputFieldStyle;\n}\nexport const Field = React.forwardRef(\n (props, ref) => {\n const {\n children,\n // Not every component that uses supports help text.\n description,\n errorMessage,\n descriptionProps = {},\n errorMessageProps = {},\n startAdornment,\n endAdornment,\n adornmentPosition,\n startAppend,\n endAppend,\n fieldClassNames,\n disabled,\n wrapperProps,\n } = props;\n\n return (\n
\n
\n );\n }\n);\n\nfunction Label({\n labelElementType,\n fieldClassNames,\n labelProps,\n label,\n labelSuffix,\n required,\n}: Omit) {\n if (!label) {\n return null;\n }\n const ElementType = labelElementType || 'label';\n const labelNode = (\n \n {label}\n {required && *}\n \n );\n\n if (labelSuffix) {\n return (\n
\n {labelNode}\n
{labelSuffix}
\n
\n );\n }\n\n return labelNode;\n}\n\ninterface AppendProps {\n children: ReactElement;\n style: InputFieldStyle['append'];\n disabled?: boolean;\n}\nfunction Append({children, style, disabled}: AppendProps) {\n return React.cloneElement(children, {\n ...children.props,\n disabled: children.props.disabled || disabled,\n // make sure append styles are not overwritten with empty values\n ...removeEmptyValuesFromObject(style),\n });\n}\n","import {RefObject, useEffect, useRef} from 'react';\n\nexport interface AutoFocusProps {\n autoFocus?: boolean;\n autoSelectText?: boolean;\n disabled?: boolean;\n}\nexport function useAutoFocus(\n {autoFocus, autoSelectText}: AutoFocusProps,\n ref: RefObject\n) {\n const autoFocusRef = useRef(autoFocus);\n\n useEffect(() => {\n if (autoFocusRef.current && ref.current) {\n // run inside animation frame to prevent issues when opening\n // dialog with via keyboard shortcut and focusing input\n requestAnimationFrame(() => {\n ref.current?.focus();\n if (autoSelectText && ref.current?.nodeName.toLowerCase() === 'input') {\n (ref.current as HTMLInputElement).select();\n }\n });\n }\n autoFocusRef.current = false;\n }, [ref, autoSelectText]);\n}\n","import {HTMLAttributes, HTMLProps, RefObject, useId} from 'react';\nimport {BaseFieldPropsWithDom} from './base-field-props';\nimport {useAutoFocus} from '../../focus/use-auto-focus';\nimport type {FieldProps} from './field';\n\ninterface UseFieldReturn {\n fieldProps: Omit;\n inputProps: HTMLAttributes;\n}\n\ninterface Props extends BaseFieldPropsWithDom {\n focusRef: RefObject;\n}\nexport function useField(props: Props): UseFieldReturn {\n const {\n focusRef,\n labelElementType = 'label',\n label,\n labelSuffix,\n autoFocus,\n autoSelectText,\n labelPosition,\n descriptionPosition,\n size,\n errorMessage,\n description,\n flexibleHeight,\n startAdornment,\n endAdornment,\n startAppend,\n adornmentPosition,\n endAppend,\n className,\n inputClassName,\n inputWrapperClassName,\n unstyled,\n background,\n invalid,\n disabled,\n id,\n inputRadius,\n inputBorder,\n inputShadow,\n inputRing,\n inputFontSize,\n ...inputDomProps\n } = props;\n\n useAutoFocus(props, focusRef);\n\n const defaultId = useId();\n const inputId = id || defaultId;\n const labelId = `${inputId}-label`;\n const descriptionId = `${inputId}-description`;\n const errorId = `${inputId}-error`;\n\n const labelProps = {\n id: labelId,\n htmlFor: labelElementType === 'label' ? inputId : undefined,\n };\n const descriptionProps = {\n id: descriptionId,\n };\n const errorMessageProps = {\n id: errorId,\n };\n\n const ariaLabel =\n !props.label && !props['aria-label'] && props.placeholder\n ? props.placeholder\n : props['aria-label'];\n\n const inputProps: HTMLProps = {\n 'aria-label': ariaLabel,\n 'aria-invalid': invalid || undefined,\n id: inputId,\n disabled,\n ...inputDomProps,\n };\n\n const labelledBy = [];\n if (label) {\n labelledBy.push(labelProps.id);\n }\n if (inputProps['aria-labelledby']) {\n labelledBy.push(inputProps['aria-labelledby']);\n }\n inputProps['aria-labelledby'] = labelledBy.length\n ? labelledBy.join(' ')\n : undefined;\n\n const describedBy = [];\n if (description) {\n describedBy.push(descriptionProps.id);\n }\n if (errorMessage) {\n describedBy.push(errorMessageProps.id);\n }\n if (inputProps['aria-describedby']) {\n describedBy.push(inputProps['aria-describedby']);\n }\n inputProps['aria-describedby'] = describedBy.length\n ? describedBy.join(' ')\n : undefined;\n\n return {\n fieldProps: {\n errorMessageProps,\n descriptionProps,\n labelProps,\n disabled,\n label,\n labelSuffix,\n autoFocus,\n autoSelectText,\n labelPosition,\n descriptionPosition,\n size,\n errorMessage,\n description,\n flexibleHeight,\n startAdornment,\n endAdornment,\n startAppend,\n adornmentPosition,\n endAppend,\n className,\n inputClassName,\n inputWrapperClassName,\n unstyled,\n background,\n invalid,\n },\n inputProps,\n };\n}\n","import React, {forwardRef, HTMLProps, Ref} from 'react';\nimport {useController} from 'react-hook-form';\nimport {mergeProps, useObjectRef} from '@react-aria/utils';\nimport {BaseFieldPropsWithDom} from '../base-field-props';\nimport {getInputFieldClassNames} from '../get-input-field-class-names';\nimport {Field} from '../field';\nimport {useField} from '../use-field';\n\nexport interface TextFieldProps\n extends BaseFieldPropsWithDom {\n rows?: number;\n inputElementType?: 'input' | 'textarea';\n inputRef?: Ref;\n value?: string | number;\n onChange?: (e: React.ChangeEvent) => void;\n}\nexport const TextField = forwardRef(\n (\n {\n inputElementType = 'input',\n flexibleHeight,\n inputRef,\n inputTestId,\n ...props\n },\n ref\n ) => {\n const inputObjRef = useObjectRef(inputRef);\n\n const {fieldProps, inputProps} = useField({\n ...props,\n focusRef: inputObjRef,\n });\n\n const isTextArea = inputElementType === 'textarea';\n const ElementType: React.ElementType = isTextArea ? 'textarea' : 'input';\n const inputFieldClassNames = getInputFieldClassNames({\n ...props,\n flexibleHeight: flexibleHeight || inputElementType === 'textarea',\n });\n\n if (inputElementType === 'textarea' && !props.unstyled) {\n inputFieldClassNames.input = `${inputFieldClassNames.input} py-12`;\n }\n\n return (\n \n ).rows || 4\n : undefined\n }\n className={inputFieldClassNames.input}\n />\n \n );\n }\n);\n\nexport interface FormTextFieldProps extends TextFieldProps {\n name: string;\n}\nexport const FormTextField = React.forwardRef<\n HTMLDivElement,\n FormTextFieldProps\n>(({name, ...props}, ref) => {\n const {\n field: {onChange, onBlur, value = '', ref: inputRef},\n fieldState: {invalid, error},\n } = useController({\n name,\n });\n\n const formProps: TextFieldProps = {\n onChange,\n onBlur,\n value: value == null ? '' : value, // avoid issues with \"null\" value when setting form defaults from backend model\n invalid,\n errorMessage: error?.message,\n inputRef,\n name,\n };\n\n return ;\n});\n","import React, {cloneElement, forwardRef, ReactElement, useId} from 'react';\nimport {useListbox} from '@common/ui/forms/listbox/use-listbox';\nimport {Item} from '@common/ui/forms/listbox/item';\nimport {Section} from '@common/ui/forms/listbox/section';\nimport {Listbox} from '@common/ui/forms/listbox/listbox';\nimport {useListboxKeyboardNavigation} from '@common/ui/forms/listbox/use-listbox-keyboard-navigation';\nimport {createEventHandler} from '@common/utils/dom/create-event-handler';\nimport {useTypeSelect} from '@common/ui/forms/listbox/use-type-select';\nimport {ListBoxChildren, ListboxProps} from '@common/ui/forms/listbox/types';\nimport {useIsMobileMediaQuery} from '@common/utils/hooks/is-mobile-media-query';\nimport {SearchIcon} from '@common/icons/material/Search';\nimport {TextField} from '@common/ui/forms/input-field/text-field/text-field';\n\ntype Props = ListboxProps & {\n searchPlaceholder?: string;\n showSearchField?: boolean;\n children: [ReactElement, ReactElement>];\n};\nexport const MenuTrigger = forwardRef(\n (props, ref) => {\n const {\n searchPlaceholder,\n showSearchField,\n children: [menuTrigger, menu],\n floatingWidth = 'auto',\n isLoading,\n } = props;\n\n const id = useId();\n\n const isMobile = useIsMobileMediaQuery();\n const listbox = useListbox(\n {\n ...props,\n clearInputOnItemSelection: true,\n showEmptyMessage: showSearchField,\n // on mobile menu will be shown as bottom drawer, so make it fullscreen width always\n floatingWidth: isMobile ? 'auto' : floatingWidth,\n virtualFocus: showSearchField,\n role: showSearchField ? 'listbox' : 'menu',\n loopFocus: !showSearchField,\n children: menu.props.children,\n },\n ref\n );\n\n const {\n state: {isOpen, setIsOpen, activeIndex, inputValue, setInputValue},\n listboxId,\n focusItem,\n listContent,\n reference,\n onInputChange,\n } = listbox;\n\n const {\n handleTriggerKeyDown,\n handleListboxKeyboardNavigation,\n handleListboxSearchFieldKeydown,\n } = useListboxKeyboardNavigation(listbox);\n const {findMatchingItem} = useTypeSelect();\n\n // focus matching item when user types, if dropdown is open\n const handleListboxTypeSelect = (e: React.KeyboardEvent) => {\n if (!isOpen) return;\n const i = findMatchingItem(e, listContent, activeIndex);\n if (i != null) {\n focusItem('increment', i);\n }\n };\n\n return (\n setInputValue('') : undefined}\n aria-labelledby={id}\n isLoading={isLoading}\n searchField={\n showSearchField ? (\n }\n className=\"flex-shrink-0 px-8 pb-8 pt-4\"\n autoFocus\n aria-expanded={isOpen ? 'true' : 'false'}\n aria-haspopup=\"listbox\"\n aria-controls={isOpen ? listboxId : undefined}\n aria-autocomplete=\"list\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck=\"false\"\n value={inputValue}\n onChange={onInputChange}\n onKeyDown={e => {\n handleListboxSearchFieldKeydown(e);\n }}\n />\n ) : null\n }\n >\n {cloneElement(menuTrigger, {\n id,\n 'aria-expanded': isOpen ? 'true' : 'false',\n 'aria-haspopup': 'menu',\n 'aria-controls': isOpen ? listboxId : undefined,\n ref: reference,\n onKeyDown: handleTriggerKeyDown,\n onClick: createEventHandler(e => {\n menuTrigger.props?.onClick?.(e);\n setIsOpen(!isOpen);\n }),\n })}\n \n );\n }\n);\n\nexport function Menu({children}: ListBoxChildren) {\n return children as unknown as ReactElement;\n}\n\nexport {Item as MenuItem};\nexport {Section as MenuSection};\n","import React, {ReactElement, useEffect} from 'react';\nimport {useListbox} from '../../forms/listbox/use-listbox';\nimport {Listbox} from '../../forms/listbox/listbox';\nimport {Menu} from './menu-trigger';\nimport {useListboxKeyboardNavigation} from '../../forms/listbox/use-listbox-keyboard-navigation';\nimport {useTypeSelect} from '../../forms/listbox/use-type-select';\nimport {ListBoxChildren, ListboxProps} from '../../forms/listbox/types';\nimport {VirtualElement} from '@floating-ui/react-dom';\n\nconst preventContextOnMenu = (e: MouseEvent) => {\n e.preventDefault();\n};\n\ntype Props = ListboxProps &\n ListBoxChildren & {\n position?: {x: number; y: number} | null;\n };\n\nexport function ContextMenu({position, children, ...props}: Props) {\n const listbox = useListbox({\n ...props,\n isOpen: props.isOpen && !!position,\n placement: 'right-start',\n floatingWidth: 'auto',\n offset: {mainAxis: 5, alignmentAxis: 4},\n role: 'menu',\n loopFocus: true,\n children:\n (children as ReactElement)?.type === Menu\n ? (children as ReactElement).props.children\n : children,\n });\n const {\n reference,\n refs,\n state: {isOpen, setIsOpen, activeIndex},\n focusItem,\n listContent,\n } = listbox;\n\n useEffect(() => {\n if (refs.floating.current) {\n refs.floating.current.addEventListener(\n 'contextmenu',\n preventContextOnMenu\n );\n return () => {\n refs.floating.current?.removeEventListener(\n 'contextmenu',\n preventContextOnMenu\n );\n };\n }\n }, [refs.floating]);\n\n useEffect(() => {\n if (position) {\n reference(pointToVirtualElement(position));\n setIsOpen(true);\n }\n }, [position, reference, setIsOpen]);\n\n const {handleListboxKeyboardNavigation} =\n useListboxKeyboardNavigation(listbox);\n\n const {findMatchingItem} = useTypeSelect();\n\n return (\n {\n if (!isOpen) return;\n const i = findMatchingItem(e, listContent, activeIndex);\n if (i) {\n focusItem('increment', i);\n }\n }}\n onKeyDown={handleListboxKeyboardNavigation}\n />\n );\n}\n\nexport function pointToVirtualElement(\n {x, y}: {x: number; y: number},\n contextElement?: Element\n): VirtualElement {\n return {\n getBoundingClientRect() {\n return {\n x,\n y,\n width: 0,\n height: 0,\n top: y,\n right: x,\n bottom: y,\n left: x,\n };\n },\n contextElement,\n };\n}\n","import {useEffect, useMemo, useRef} from 'react';\n\n/**\n * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a\n * prop or avoid re-executing effects when passed as a dependency\n */\nexport function useCallbackRef any>(\n callback: T | undefined\n): T {\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n });\n\n // https://github.com/facebook/react/issues/19240\n return useMemo(() => ((...args) => callbackRef.current?.(...args)) as T, []);\n}\n","import React, {\n Children,\n cloneElement,\n Fragment,\n HTMLProps,\n ReactElement,\n ReactNode,\n RefObject,\n useCallback,\n useId,\n useMemo,\n useRef,\n} from 'react';\nimport {AnimatePresence} from 'framer-motion';\nimport {useControlledState} from '@react-stately/utils';\nimport {mergeProps, useLayoutEffect} from '@react-aria/utils';\nimport {useFloatingPosition} from '../floating-position';\nimport {useIsMobileMediaQuery} from '@common/utils/hooks/is-mobile-media-query';\nimport {DialogContext, DialogContextValue} from './dialog-context';\nimport {Popover} from '../popover';\nimport {Tray} from '../tray';\nimport {Modal} from '../modal';\nimport {createPortal} from 'react-dom';\nimport {createEventHandler} from '@common/utils/dom/create-event-handler';\nimport {OffsetOptions, Placement, VirtualElement} from '@floating-ui/react-dom';\nimport {rootEl} from '@common/core/root-el';\nimport {pointToVirtualElement} from '@common/ui/navigation/menu/context-menu';\nimport {useCallbackRef} from '@common/utils/hooks/use-callback-ref';\n\ntype PopoverProps = {\n type: 'popover';\n mobileType?: 'tray' | 'modal';\n placement?: Placement;\n offset?: OffsetOptions;\n};\ntype ModalProps = {\n type: 'modal' | 'tray';\n mobileType?: 'tray' | 'modal';\n placement?: Placement;\n};\ntype Props = (PopoverProps | ModalProps) & {\n children: [ReactElement, (ctx: DialogContextValue) => void] | ReactNode;\n disableInitialTransition?: boolean;\n onClose?: (value?: T) => void;\n isDismissable?: boolean;\n isOpen?: boolean;\n onOpenChange?: (isOpen: boolean) => void;\n defaultIsOpen?: boolean;\n triggerRef?: RefObject | RefObject;\n moveFocusToDialog?: boolean;\n returnFocusToTrigger?: boolean;\n triggerOnHover?: boolean;\n triggerOnContextMenu?: boolean;\n currentValue?: T;\n usePortal?: boolean;\n};\nexport function DialogTrigger(props: Props) {\n let {\n children,\n type,\n disableInitialTransition,\n isDismissable = true,\n moveFocusToDialog = true,\n returnFocusToTrigger = true,\n triggerOnHover = false,\n currentValue,\n triggerOnContextMenu = false,\n usePortal = true,\n mobileType,\n } = props;\n\n // for context menu we will set triggerRef to VirtualElement in \"onContextMenu\" event.\n // If dialog is not triggered on context menu, leave triggerRef null (unless it's passed in via props)\n // otherwise it will prevent dialog from opening in \"popover\" mode.\n const contextMenuTriggerRef = useRef(null);\n const triggerRef =\n triggerOnContextMenu && !props.triggerRef\n ? contextMenuTriggerRef\n : props.triggerRef;\n const initialValueRef = useRef(currentValue);\n const [isOpen, setIsOpen] = useControlledState(\n props.isOpen,\n props.defaultIsOpen,\n props.onOpenChange,\n );\n\n // On small devices, show a modal or tray instead of a popover.\n const isMobile = useIsMobileMediaQuery();\n if (isMobile && type === 'popover') {\n type = mobileType || 'modal';\n }\n\n const hoverTimeoutRef = useRef(null);\n const {x, y, reference, strategy, refs} = useFloatingPosition({\n ...props,\n disablePositioning: type === 'modal',\n });\n\n const floatingStyle =\n type === 'popover'\n ? {\n position: strategy,\n top: y ?? '',\n left: x ?? '',\n }\n : {};\n\n const id = useId();\n const labelId = `${id}-label`;\n const descriptionId = `${id}-description`;\n const formId = `${id}-form`;\n\n const onClose = useCallbackRef(props.onClose);\n const close = useCallback(\n (value?: any) => {\n // initial value can be used to restore state to what it was before opening the dialog, for example in color picker\n onClose?.(value ?? initialValueRef.current);\n setIsOpen(false);\n },\n [onClose, setIsOpen],\n );\n\n const open = useCallback(() => {\n setIsOpen(true);\n // set current value that is active at the time of opening dialog\n initialValueRef.current = currentValue;\n }, [currentValue, setIsOpen]);\n\n // position dropdown relative to provided ref, not the trigger\n useLayoutEffect(() => {\n if (triggerRef?.current && refs.reference.current !== triggerRef.current) {\n reference(triggerRef.current);\n }\n }, [reference, triggerRef?.current, refs]);\n\n const dialogProps = useMemo(() => {\n return {\n 'aria-labelledby': labelId,\n 'aria-describedby': descriptionId,\n };\n }, [labelId, descriptionId]);\n\n let Overlay: typeof Modal | typeof Tray | typeof Popover;\n if (type === 'modal') {\n Overlay = Modal;\n } else if (type === 'tray') {\n Overlay = Tray;\n } else {\n Overlay = Popover;\n }\n\n const contextValue: DialogContextValue = useMemo(() => {\n return {\n dialogProps,\n type,\n labelId,\n descriptionId,\n isDismissable,\n close,\n formId,\n };\n }, [close, descriptionId, dialogProps, formId, labelId, type, isDismissable]);\n\n triggerOnHover = triggerOnHover && type === 'popover';\n\n const handleTriggerHover: HTMLProps = {\n onPointerEnter: createEventHandler((e: React.PointerEvent) => {\n open();\n }),\n onPointerLeave: createEventHandler((e: React.PointerEvent) => {\n hoverTimeoutRef.current = setTimeout(() => {\n close();\n }, 150);\n }),\n };\n\n const handleFloatingHover: HTMLProps = {\n onPointerEnter: createEventHandler((e: React.PointerEvent) => {\n if (hoverTimeoutRef.current) {\n clearTimeout(hoverTimeoutRef.current);\n }\n }),\n onPointerLeave: createEventHandler((e: React.PointerEvent) => {\n close();\n }),\n };\n\n const handleTriggerContextMenu: HTMLProps = {\n onContextMenu: createEventHandler((e: React.MouseEvent) => {\n e.preventDefault();\n contextMenuTriggerRef.current = pointToVirtualElement(\n {x: e.clientX, y: e.clientY},\n e.currentTarget,\n );\n open();\n }),\n };\n\n const handleTriggerClick: HTMLProps = {\n onClick: createEventHandler((e: React.MouseEvent) => {\n // prevent propagating to parent, in case floating element\n // is attached to input field and button is inside the field\n e.stopPropagation();\n if (isOpen) {\n close();\n } else {\n open();\n }\n }),\n };\n\n const {dialogTrigger, dialog} = extractChildren(children, contextValue);\n\n const dialogContent = (\n \n {isOpen && (\n \n \n {dialog}\n \n \n )}\n \n );\n\n return (\n \n {dialogTrigger &&\n cloneElement(\n dialogTrigger,\n mergeProps(\n {\n // make sure ref specified on trigger element is not overwritten\n ...(!triggerRef && !triggerOnContextMenu ? {ref: reference} : {}),\n ...(!triggerOnContextMenu ? handleTriggerClick : {}),\n ...(triggerOnHover ? handleTriggerHover : {}),\n ...(triggerOnContextMenu ? handleTriggerContextMenu : {}),\n },\n {\n ...dialogTrigger.props,\n },\n ),\n )}\n {usePortal\n ? rootEl && createPortal(dialogContent, rootEl)\n : dialogContent}\n \n );\n}\n\nfunction extractChildren(\n rawChildren: Props['children'],\n ctx: DialogContextValue,\n) {\n const children = Array.isArray(rawChildren)\n ? rawChildren\n : Children.toArray(rawChildren);\n\n let dialog: any = children.length === 2 ? children[1] : children[0];\n dialog = typeof dialog === 'function' ? dialog(ctx) : dialog;\n\n // trigger and dialog passed as children\n if (children.length === 2) {\n return {\n dialogTrigger: children[0] as ReactElement,\n dialog: dialog as ReactElement,\n };\n }\n\n // only dialog passed as child\n return {dialog: dialog as ReactElement};\n}\n","import {\n closeDialog,\n useDialogStore,\n} from '@common/ui/overlays/store/dialog-store';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport React from 'react';\n\nexport function DialogStoreOutlet() {\n const {dialog: DialogElement, data} = useDialogStore();\n return (\n {\n closeDialog(value);\n }}\n >\n {DialogElement ? : null}\n \n );\n}\n","import {useCallback, useEffect} from 'react';\nimport {useNavigate} from 'react-router-dom';\nimport {AllCommands} from './commands';\nimport {setThemeColor} from '../../../ui/themes/utils/set-theme-color';\nimport {applyThemeToDom} from '../../../ui/themes/utils/apply-theme-to-dom';\nimport {useBootstrapData} from '../../../core/bootstrap-data/bootstrap-data-context';\n\nexport function AppearanceListener() {\n const navigate = useNavigate();\n const {mergeBootstrapData, data: currentData} = useBootstrapData();\n\n const handleCommand = useCallback(\n (command: AllCommands) => {\n switch (command.type) {\n case 'navigate':\n return navigate(command.to);\n case 'setValues':\n return mergeBootstrapData({\n themes: {\n ...currentData.themes,\n all: command.values.appearance.themes.all,\n },\n settings: {\n ...currentData.settings,\n ...command.values.settings,\n },\n });\n case 'setThemeColor':\n return setThemeColor(command.name, command.value);\n case 'setActiveTheme':\n const theme = currentData.themes.all.find(\n t => t.id === command.themeId\n );\n if (theme) {\n applyThemeToDom(theme);\n }\n return;\n case 'setCustomCode':\n return renderCustomCode(command.mode, command.value);\n default:\n }\n },\n [currentData, mergeBootstrapData, navigate]\n );\n\n useEffect(() => {\n const handleMessage = (e: MessageEvent) => {\n if (isAppearanceEvent(e) && eventIsTrusted(e)) {\n handleCommand(e.data);\n }\n };\n window.addEventListener('message', handleMessage);\n return () => {\n window.removeEventListener('message', handleMessage);\n };\n }, [navigate, handleCommand]);\n return null;\n}\n\nfunction isAppearanceEvent(e: MessageEvent) {\n return e.data?.source === 'be-appearance-editor';\n}\n\nfunction eventIsTrusted(e: MessageEvent): boolean {\n return new URL(e.origin).hostname === window.location.hostname;\n}\n\nfunction renderCustomCode(mode: 'html' | 'css', value?: string) {\n const parent = mode === 'html' ? document.body : document.head;\n const nodeType = mode === 'html' ? 'div' : 'style';\n let customNode = parent.querySelector('#be-custom-code');\n\n if (!value) {\n if (customNode) {\n customNode.remove();\n }\n } else {\n if (!customNode) {\n customNode = document.createElement(nodeType);\n customNode.id = 'be-custom-code';\n parent.appendChild(customNode);\n }\n customNode.innerHTML = value;\n }\n}\n","import {MenuConfig} from '../core/settings/settings';\nimport {useAuth} from '../auth/use-auth';\nimport {useSettings} from '../core/settings/use-settings';\n\nexport function useCustomMenu(menuOrPosition?: string | MenuConfig) {\n const settings = useSettings();\n const {user, hasPermission} = useAuth();\n\n if (!menuOrPosition) {\n return null;\n }\n\n const menu =\n typeof menuOrPosition === 'string'\n ? settings.menus?.find(s => s.positions?.includes(menuOrPosition))\n : menuOrPosition;\n\n if (menu) {\n menu.items = menu.items.filter(item => {\n const hasRoles = (item.roles || []).every(a =>\n user?.roles.find(b => b.id === a)\n );\n const hasPermissions = (item.permissions || []).every(a =>\n hasPermission(a)\n );\n // make sure item has action, otherwise router link will error out\n return item.action && hasRoles && hasPermissions;\n });\n }\n return menu;\n}\n","import React, {forwardRef, Fragment, ReactElement} from 'react';\nimport {NavLink} from 'react-router-dom';\nimport clsx from 'clsx';\nimport {MenuConfig, MenuItemConfig} from '../core/settings/settings';\nimport {createSvgIconFromTree} from '../icons/create-svg-icon';\nimport {Orientation} from '../ui/forms/orientation';\nimport {useCustomMenu} from './use-custom-menu';\nimport {Trans} from '../i18n/trans';\nimport {IconSize} from '@common/icons/svg-icon';\n\ntype MatchDescendants = undefined | boolean | ((to: string) => boolean);\n\ninterface CustomMenuProps {\n className?: string;\n matchDescendants?: MatchDescendants;\n iconClassName?: string;\n iconSize?: IconSize;\n itemClassName?:\n | string\n | ((props: {\n isActive: boolean;\n item: MenuItemConfig;\n }) => string | undefined);\n gap?: string;\n menu?: string | MenuConfig;\n children?: (menuItem: MenuItemConfig) => ReactElement;\n orientation?: Orientation;\n onlyShowIcons?: boolean;\n unstyled?: boolean;\n}\nexport function CustomMenu({\n className,\n iconClassName,\n itemClassName,\n gap = 'gap-30',\n menu: menuOrPosition,\n orientation = 'horizontal',\n children,\n matchDescendants,\n onlyShowIcons,\n iconSize,\n unstyled = false,\n}: CustomMenuProps) {\n const menu = useCustomMenu(menuOrPosition);\n if (!menu) return null;\n\n return (\n \n {menu.items.map(item => {\n if (children) {\n return children(item);\n }\n return (\n {\n return typeof itemClassName === 'function'\n ? itemClassName({...props, item})\n : itemClassName;\n }}\n key={item.id}\n item={item}\n />\n );\n })}\n \n );\n}\n\ninterface MenuItemProps extends React.RefAttributes {\n item: MenuItemConfig;\n iconClassName?: string;\n className?: (props: {isActive: boolean}) => string | undefined;\n matchDescendants?: MatchDescendants;\n onlyShowIcon?: boolean;\n iconSize?: IconSize;\n unstyled?: boolean;\n}\nexport const CustomMenuItem = forwardRef(\n (\n {\n item,\n className,\n matchDescendants,\n unstyled,\n onlyShowIcon,\n iconClassName,\n iconSize = 'sm',\n ...linkProps\n },\n ref\n ) => {\n const label = ;\n const Icon = item.icon && createSvgIconFromTree(item.icon);\n const content = (\n \n {Icon && }\n {(!Icon || !onlyShowIcon) && label}\n \n );\n\n const baseClassName =\n !unstyled &&\n 'block whitespace-nowrap flex items-center justify-start gap-10';\n\n const focusClassNames = !unstyled && 'outline-none focus-visible:ring-2';\n\n if (item.type === 'link') {\n return (\n \n {content}\n \n );\n }\n return (\n \n clsx(baseClassName, className?.(props), focusClassNames)\n }\n to={item.action}\n target={item.target}\n data-menu-item-id={item.id}\n ref={ref}\n {...linkProps}\n >\n {content}\n \n );\n }\n);\n","import clsx from 'clsx';\nimport {Trans} from '../../i18n/trans';\nimport {CustomMenuItem} from '../../menus/custom-menu';\nimport {Button} from '../buttons/button';\nimport {useSettings} from '../../core/settings/use-settings';\nimport {useState} from 'react';\nimport {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';\nimport {useCookie} from '@common/utils/hooks/use-cookie';\n\nexport function CookieNotice() {\n const {\n cookie_notice: {position, enable},\n } = useSettings();\n\n const [, setCookie] = useCookie('cookie_notice');\n\n const [alreadyAccepted, setAlreadyAccepted] = useState(() => {\n return !getBootstrapData().show_cookie_notice;\n });\n\n if (!enable || alreadyAccepted) {\n return null;\n }\n\n return (\n \n \n \n {\n setCookie('true', {days: 30, path: '/'});\n setAlreadyAccepted(true);\n }}\n >\n \n \n \n );\n}\n\nfunction InfoLink() {\n const {\n cookie_notice: {button},\n } = useSettings();\n\n if (!button?.label) {\n return null;\n }\n\n return (\n 'text-primary-light hover:underline'}\n item={button}\n />\n );\n}\n","import {useAuth} from '../use-auth';\nimport {ReactElement} from 'react';\nimport {Navigate, Outlet, useLocation} from 'react-router-dom';\nimport {useAppearanceEditorMode} from '../../admin/appearance/commands/use-appearance-editor-mode';\n\ninterface GuestRouteProps {\n children: ReactElement;\n}\nexport function GuestRoute({children}: GuestRouteProps) {\n const {isLoggedIn, getRedirectUri} = useAuth();\n const {isAppearanceEditorActive} = useAppearanceEditorMode();\n const redirectUri = getRedirectUri();\n const {pathname} = useLocation();\n\n if (isLoggedIn && !isAppearanceEditorActive) {\n // prevent recursive redirects\n if (redirectUri !== pathname) {\n return ;\n }\n }\n\n return children || ;\n}\n","import {\n FieldValues,\n FormProvider,\n SubmitHandler,\n UseFormReturn,\n} from 'react-hook-form';\nimport {FocusEventHandler, ReactNode} from 'react';\n\ninterface Props {\n children: ReactNode;\n form: UseFormReturn;\n className?: string;\n onSubmit: SubmitHandler;\n onBeforeSubmit?: () => void;\n onBlur?: FocusEventHandler;\n id?: string;\n}\nexport function Form({\n children,\n onBeforeSubmit,\n onSubmit,\n form,\n className,\n id,\n onBlur,\n}: Props) {\n return (\n \n {\n // prevent parent forms from submitting, if nested\n e.stopPropagation();\n onBeforeSubmit?.();\n form.handleSubmit(onSubmit)(e);\n }}\n >\n {children}\n \n \n );\n}\n","import {ComponentPropsWithRef} from 'react';\n\nexport const LinkStyle =\n 'text-primary hover:underline hover:text-primary-dark focus-visible:ring focus-visible:ring-2 focus-visible:ring-offset-2 outline-none rounded transition-colors';\n\ninterface ExternalLinkProps extends ComponentPropsWithRef<'a'> {}\nexport function ExternalLink({\n children,\n className,\n target = '_blank',\n ...domProps\n}: ExternalLinkProps) {\n return (\n \n {children}\n \n );\n}\n","import axios from 'axios';\nimport {UseFormReturn} from 'react-hook-form';\nimport {BackendErrorResponse} from './backend-error-response';\nimport {toast} from '../ui/toast/toast';\nimport {message} from '../i18n/message';\n\nexport function onFormQueryError(r: unknown, form: UseFormReturn) {\n if (form && axios.isAxiosError(r) && r.response) {\n const response = r.response.data as BackendErrorResponse;\n if (!response.errors) {\n toast.danger(\n response.message ??\n message('There was an issue. Please try again later.')\n );\n } else {\n Object.entries(response.errors || {}).forEach(([key, errors], index) => {\n if (typeof errors === 'string') {\n form.setError(key, {message: errors}, {shouldFocus: index === 0});\n } else {\n errors.forEach((message, subIndex) => {\n form.setError(\n key,\n {message},\n {shouldFocus: index === 0 && subIndex === 0}\n );\n });\n }\n });\n }\n }\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {onFormQueryError} from '../../errors/on-form-query-error';\nimport {useNavigate} from '../../utils/hooks/use-navigate';\nimport {apiClient} from '../../http/query-client';\nimport {useAuth} from '../use-auth';\nimport {useBootstrapData} from '../../core/bootstrap-data/bootstrap-data-context';\n\ninterface Response extends BackendResponse {\n bootstrapData?: string;\n message?: string;\n status: 'success' | 'needs_email_verification';\n}\n\nexport interface RegisterPayload {\n email: string;\n password: string;\n password_confirmation: string;\n}\n\nexport function useRegister(form: UseFormReturn) {\n const navigate = useNavigate();\n const {getRedirectUri} = useAuth();\n const {setBootstrapData} = useBootstrapData();\n\n return useMutation({\n mutationFn: register,\n onSuccess: response => {\n setBootstrapData(response.bootstrapData!);\n if (response.status === 'needs_email_verification') {\n navigate('/');\n } else {\n navigate(getRedirectUri(), {replace: true});\n }\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction register(payload: RegisterPayload): Promise {\n return apiClient\n .post('auth/register', payload)\n .then(response => response.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {onFormQueryError} from '../../errors/on-form-query-error';\nimport {useNavigate} from '../../utils/hooks/use-navigate';\nimport {apiClient} from '../../http/query-client';\nimport {useAuth} from '../use-auth';\nimport {useBootstrapData} from '../../core/bootstrap-data/bootstrap-data-context';\n\ninterface Response extends BackendResponse {\n bootstrapData: string;\n}\n\nexport interface ConnectSocialPayload {\n password: string;\n}\n\nexport function useConnectSocialWithPassword(\n form: UseFormReturn,\n) {\n const navigate = useNavigate();\n const {getRedirectUri} = useAuth();\n const {setBootstrapData} = useBootstrapData();\n return useMutation({\n mutationFn: connect,\n onSuccess: response => {\n setBootstrapData(response.bootstrapData);\n navigate(getRedirectUri(), {replace: true});\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction connect(payload: ConnectSocialPayload): Promise {\n return apiClient\n .post('secure/auth/social/connect', payload)\n .then(response => response.data);\n}\n","import {useCallback} from 'react';\nimport memoize from 'nano-memoize';\nimport {useSelectedLocale} from './selected-locale';\nimport {handlePluralMessage} from './handle-plural-message';\nimport {MessageDescriptor} from './message-descriptor';\nimport {shallowEqual} from '../utils/shallow-equal';\n\nexport interface UseTransReturn {\n trans: (props: MessageDescriptor) => string;\n}\n\nexport function useTrans(): UseTransReturn {\n const {lines, localeCode} = useSelectedLocale();\n const trans = useCallback(\n (props: MessageDescriptor): string => {\n return translate({...props, lines, localeCode});\n },\n [lines, localeCode],\n );\n\n return {trans};\n}\n\ninterface TranslateProps extends MessageDescriptor {\n lines?: Record;\n localeCode: string;\n}\nconst translate = memoize(\n (props: TranslateProps) => {\n let {lines, message, values, localeCode} = props;\n message = lines?.[message] || lines?.[message.toLowerCase()] || message;\n\n if (!values) {\n return message;\n }\n\n message = handlePluralMessage(localeCode, props);\n\n Object.entries(values).forEach(([key, value]) => {\n message = message.replace(`:${key}`, `${value}`);\n });\n\n return message;\n },\n {equals: shallowEqual, callTimeout: 0},\n);\n","import React from 'react';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {message} from '../../../i18n/message';\n\ninterface DismissButtonProps {\n onDismiss?: () => void;\n}\nexport function DismissButton({onDismiss}: DismissButtonProps) {\n const {trans} = useTrans();\n\n const onClick = () => {\n if (onDismiss) {\n onDismiss();\n }\n };\n\n return (\n \n );\n}\n","import React, {\n Children,\n cloneElement,\n ComponentPropsWithoutRef,\n CSSProperties,\n isValidElement,\n ReactElement,\n ReactNode,\n useContext,\n} from 'react';\nimport clsx from 'clsx';\nimport {mergeProps} from '@react-aria/utils';\nimport {DialogContext} from './dialog-context';\nimport {InputSize} from '../../forms/input-field/input-size';\nimport {DismissButton} from './dismiss-button';\n\nexport type DialogSize =\n | InputSize\n | '2xl'\n | 'auto'\n | 'fullscreen'\n | 'fullscreenTakeover'\n | string;\n\nexport interface DialogProps\n extends Omit, 'size'> {\n children: ReactNode;\n size?: DialogSize;\n background?: string;\n className?: string;\n radius?: string;\n maxWidth?: string;\n}\nexport function Dialog(props: DialogProps) {\n const {\n type = 'modal',\n dialogProps,\n ...contextProps\n } = useContext(DialogContext);\n\n const {\n children,\n className,\n size = 'md',\n background,\n radius = 'rounded',\n maxWidth = 'max-w-dialog',\n ...domProps\n } = props;\n\n // If rendered in a popover or tray there won't be a visible dismiss button,\n // so we render a hidden one for screen readers.\n let dismissButton: ReactElement | null = null;\n if (type === 'popover' || type === 'tray') {\n dismissButton = ;\n }\n\n const isTrayOrFullScreen = size === 'fullscreenTakeover' || type === 'tray';\n const mergedClassName = clsx(\n 'mx-auto pointer-events-auto outline-none flex flex-col overflow-hidden',\n background || 'bg-paper',\n type !== 'tray' && sizeStyle(size),\n type === 'tray' && 'rounded-t',\n size !== 'fullscreenTakeover' && `shadow-2xl border max-h-dialog`,\n !isTrayOrFullScreen && `${radius} ${maxWidth}`,\n className\n );\n\n return (\n \n {Children.toArray(children).map(child => {\n if (isValidElement(child)) {\n return cloneElement(child, {\n size: child.props.size ?? size,\n });\n }\n return child;\n })}\n {dismissButton}\n \n );\n}\n\nfunction sizeStyle(dialogSize?: DialogSize) {\n switch (dialogSize) {\n case '2xs':\n return 'w-256';\n case 'xs':\n return 'w-320';\n case 'sm':\n return 'w-384';\n case 'md':\n return 'w-440';\n case 'lg':\n return 'w-620';\n case 'xl':\n return 'w-780';\n case '2xl':\n return 'w-850';\n case 'fullscreen':\n return 'w-1280';\n case 'fullscreenTakeover':\n return 'w-full h-full';\n default:\n return dialogSize;\n }\n}\n","import React, {ReactNode, useContext} from 'react';\nimport clsx from 'clsx';\nimport {DialogContext} from './dialog-context';\nimport {IconButton} from '../../buttons/icon-button';\nimport {CloseIcon} from '../../../icons/material/Close';\nimport {DialogSize} from './dialog';\nimport {ButtonSize} from '@common/ui/buttons/button-size';\n\ninterface DialogHeaderProps {\n children: ReactNode;\n className?: string;\n color?: string | null;\n onDismiss?: () => void;\n hideDismissButton?: boolean;\n leftAdornment?: ReactNode;\n // Will hide default close button visually, but still accessible by screen readers\n rightAdornment?: ReactNode;\n // Will show between title and close button\n actions?: ReactNode;\n size?: DialogSize;\n padding?: string;\n justify?: string;\n showDivider?: boolean;\n titleTextSize?: string;\n titleFontWeight?: string;\n closeButtonSize?: ButtonSize;\n}\nexport function DialogHeader(props: DialogHeaderProps) {\n const {\n children,\n className,\n color,\n onDismiss,\n leftAdornment,\n rightAdornment,\n hideDismissButton = false,\n size,\n showDivider,\n justify = 'justify-between',\n titleFontWeight = 'font-semibold',\n titleTextSize = size === 'xs' ? 'text-xs' : 'text-sm',\n closeButtonSize = size === 'xs' ? 'xs' : 'sm',\n actions,\n } = props;\n const {labelId, isDismissable, close} = useContext(DialogContext);\n\n return (\n \n {leftAdornment}\n \n {children}\n \n {rightAdornment}\n {actions}\n {isDismissable && !hideDismissButton && (\n {\n if (onDismiss) {\n onDismiss();\n } else {\n close();\n }\n }}\n size={closeButtonSize}\n className={clsx('-mr-8 text-muted', rightAdornment && 'sr-only')}\n >\n \n \n )}\n \n );\n}\n\nfunction getPadding({size, padding}: DialogHeaderProps) {\n if (padding) {\n return padding;\n }\n switch (size) {\n case '2xs':\n case 'xs':\n return 'px-14 py-4';\n case 'sm':\n return 'px-18 py-4';\n default:\n return 'px-24 py-6';\n }\n}\n","import React, {ComponentProps, forwardRef, ReactNode} from 'react';\nimport clsx from 'clsx';\nimport {DialogSize} from './dialog';\n\ninterface DialogBodyProps extends ComponentProps<'div'> {\n children: ReactNode;\n className?: string;\n padding?: string | null;\n size?: DialogSize;\n}\nexport const DialogBody = forwardRef(\n (props, ref) => {\n const {children, className, padding, size, ...domProps} = props;\n return (\n \n {children}\n \n );\n }\n);\n\nfunction getPadding({size, padding}: DialogBodyProps) {\n if (padding) {\n return padding;\n }\n switch (size) {\n case 'xs':\n return 'p-14';\n case 'sm':\n return 'p-18';\n default:\n return 'px-24 py-20';\n }\n}\n","import React, {ReactNode} from 'react';\nimport clsx from 'clsx';\nimport {DialogSize} from './dialog';\n\ninterface DialogFooterProps {\n children: ReactNode;\n startAction?: ReactNode;\n className?: string;\n dividerTop?: boolean;\n size?: DialogSize;\n padding?: string;\n}\nexport function DialogFooter(props: DialogFooterProps) {\n const {children, startAction, className, dividerTop, padding, size} = props;\n\n return (\n \n
{startAction}
\n
{children}
\n \n );\n}\n\nfunction getPadding({padding, size}: DialogFooterProps) {\n if (padding) {\n return padding;\n }\n switch (size) {\n case 'xs':\n return 'p-14';\n case 'sm':\n return 'p-18';\n default:\n return 'px-24 py-20';\n }\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {apiClient} from '../../http/query-client';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {\n //\n}\n\ninterface Payload {\n service: string;\n}\n\nexport function useDisconnectSocial() {\n return useMutation({\n mutationFn: disconnect,\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction disconnect(payload: Payload): Promise {\n return apiClient\n .post(`secure/auth/social/${payload.service}/disconnect`, payload)\n .then(response => response.data);\n}\n","import {useCallback, useState} from 'react';\nimport {toast} from '../../ui/toast/toast';\nimport {useDisconnectSocial} from './disconnect-social';\nimport {useTrans} from '../../i18n/use-trans';\nimport {getBootstrapData} from '../../core/bootstrap-data/use-backend-bootstrap-data';\nimport {useBootstrapData} from '../../core/bootstrap-data/bootstrap-data-context';\n\nexport type SocialService = 'google' | 'twitter' | 'facebook' | 'envato';\n\ninterface SocialMessageEvent {\n status?: 'SUCCESS' | 'ALREADY_LOGGED_IN' | 'REQUEST_PASSWORD' | 'ERROR';\n callbackData?: {\n bootstrapData?: string;\n errorMessage?: string;\n };\n}\n\nexport function useSocialLogin() {\n const {trans} = useTrans();\n const {setBootstrapData} = useBootstrapData();\n const disconnectSocial = useDisconnectSocial();\n\n const [requestingPassword, setIsRequestingPassword] = useState(false);\n\n const handleSocialLoginCallback = useCallback(\n (e: SocialMessageEvent) => {\n const {status, callbackData} = e;\n if (!status) return;\n switch (status.toUpperCase()) {\n case 'SUCCESS':\n if (callbackData?.bootstrapData) {\n setBootstrapData(callbackData.bootstrapData);\n }\n return e;\n case 'REQUEST_PASSWORD':\n setIsRequestingPassword(true);\n return e;\n case 'ERROR':\n const message: string =\n callbackData?.errorMessage ||\n trans({\n message: 'An error occurred. Please try again later',\n });\n toast.danger(message);\n return e;\n default:\n return e;\n }\n },\n [trans, setBootstrapData],\n );\n\n return {\n requestingPassword,\n setIsRequestingPassword,\n loginWithSocial: async (serviceName: SocialService) => {\n const event = await openNewSocialAuthWindow(\n `secure/auth/social/${serviceName}/login`,\n );\n return handleSocialLoginCallback(event);\n },\n connectSocial: async (serviceNameOrUrl: SocialService | string) => {\n const url = serviceNameOrUrl.includes('/')\n ? serviceNameOrUrl\n : `secure/auth/social/${serviceNameOrUrl}/connect`;\n const event = await openNewSocialAuthWindow(url);\n return handleSocialLoginCallback(event);\n },\n disconnectSocial,\n };\n}\n\nconst windowHeight = 550;\nconst windowWidth = 650;\nlet win: Window | null;\n\nfunction openNewSocialAuthWindow(url: string): Promise {\n const left = window.screen.width / 2 - windowWidth / 2;\n const top = window.screen.height / 2 - windowHeight / 2;\n\n return new Promise(resolve => {\n win = window.open(\n url,\n 'Authenticate Account',\n `menubar=0, location=0, toolbar=0, titlebar=0, status=0, scrollbars=1, width=${windowWidth}, height=${windowHeight}, left=${left}, top=${top}`,\n );\n\n const messageListener = (e: MessageEvent) => {\n const baseUrl = getBootstrapData().settings.base_url;\n if (e.data.type === 'social-auth' && baseUrl.indexOf(e.origin) > -1) {\n resolve(e.data);\n window.removeEventListener('message', messageListener);\n }\n };\n\n window.addEventListener('message', messageListener);\n\n // if user closes social login callback without interacting with it, remove message event listener\n const timer = setInterval(() => {\n if (!win || win.closed) {\n clearInterval(timer);\n resolve({});\n window.removeEventListener('message', messageListener);\n }\n }, 1000);\n });\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const GoogleIcon = createSvgIcon(\n \n \n \n \n \n \n \n);\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const FacebookIcon = createSvgIcon(\n \n);\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const TwitterIcon = createSvgIcon(\n \n);\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const EnvatoIcon = createSvgIcon(\n \n);\n","import {useForm} from 'react-hook-form';\nimport {Fragment, ReactElement, ReactNode} from 'react';\nimport {\n ConnectSocialPayload,\n useConnectSocialWithPassword,\n} from '../requests/connect-social-with-password';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\nimport {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {Form} from '../../ui/forms/form';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';\nimport {Button} from '../../ui/buttons/button';\nimport {SocialService, useSocialLogin} from '../requests/use-social-login';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {GoogleIcon} from '../../icons/social/google';\nimport {FacebookIcon} from '../../icons/social/facebook';\nimport {TwitterIcon} from '../../icons/social/twitter';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {Trans} from '../../i18n/trans';\nimport {useNavigate} from '../../utils/hooks/use-navigate';\nimport {useAuth} from '../use-auth';\nimport {useTrans} from '../../i18n/use-trans';\nimport {message} from '../../i18n/message';\nimport {useSettings} from '../../core/settings/use-settings';\nimport {MessageDescriptor} from '@common/i18n/message-descriptor';\nimport clsx from 'clsx';\nimport {EnvatoIcon} from '@common/icons/social/envato';\n\nconst googleLabel = message('Continue with google');\nconst facebookLabel = message('Continue with facebook');\nconst twitterLabel = message('Continue with twitter');\nconst envatoLabel = message('Continue with envato');\n\ninterface SocialAuthSectionProps {\n dividerMessage: ReactNode;\n}\nexport function SocialAuthSection({dividerMessage}: SocialAuthSectionProps) {\n const {social} = useSettings();\n const navigate = useNavigate();\n const {getRedirectUri} = useAuth();\n const {loginWithSocial, requestingPassword, setIsRequestingPassword} =\n useSocialLogin();\n\n const allSocialsDisabled =\n !social?.google?.enable &&\n !social?.facebook?.enable &&\n !social?.twitter?.enable &&\n !social?.envato?.enable;\n\n if (allSocialsDisabled) {\n return null;\n }\n\n const handleSocialLogin = async (service: SocialService) => {\n const e = await loginWithSocial(service);\n if (e?.status === 'SUCCESS' || e?.status === 'ALREADY_LOGGED_IN') {\n navigate(getRedirectUri(), {replace: true});\n }\n };\n\n return (\n \n
\n \n {dividerMessage}\n \n
\n \n {social?.google?.enable ? (\n }\n onClick={() => handleSocialLogin('google')}\n />\n ) : null}\n {social?.facebook?.enable ? (\n }\n onClick={() => handleSocialLogin('facebook')}\n />\n ) : null}\n {social?.twitter?.enable ? (\n }\n onClick={() => handleSocialLogin('twitter')}\n />\n ) : null}\n {social?.envato?.enable ? (\n }\n onClick={() => handleSocialLogin('envato')}\n />\n ) : null}\n \n \n \n \n
\n );\n}\n\nfunction RequestPasswordDialog() {\n const form = useForm();\n const {formId} = useDialogContext();\n const connect = useConnectSocialWithPassword(form);\n return (\n \n \n \n \n \n
\n \n
\n {\n connect.mutate(payload);\n }}\n >\n }\n />\n \n
\n \n \n \n \n \n \n
\n );\n}\n\ninterface SocialLoginButtonProps {\n onClick: () => void;\n label: MessageDescriptor;\n icon: ReactElement;\n}\nfunction SocialLoginButton({onClick, label, icon}: SocialLoginButtonProps) {\n const {trans} = useTrans();\n const {\n social: {compact_buttons},\n } = useSettings();\n\n if (compact_buttons) {\n return (\n \n {icon}\n \n );\n }\n\n return (\n \n \n \n \n \n );\n}\n","import {Link} from 'react-router-dom';\nimport {CustomMenu} from '../../../menus/custom-menu';\nimport {useSettings} from '../../../core/settings/use-settings';\n\nexport function AuthLayoutFooter() {\n const {branding} = useSettings();\n return (\n
\n \n © {branding.site_name}\n \n \n
\n );\n}\n","export default \"__VITE_ASSET__b6eba7cf__\"","import {Link} from 'react-router-dom';\nimport {ReactNode} from 'react';\nimport {AuthLayoutFooter} from './auth-layout-footer';\nimport {useIsDarkMode} from '@common/ui/themes/use-is-dark-mode';\nimport authBgSvg from './auth-bg.svg';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {useSettings} from '@common/core/settings/use-settings';\n\ninterface AuthPageProps {\n heading?: ReactNode;\n message?: ReactNode;\n children: ReactNode;\n}\nexport function AuthLayout({heading, children, message}: AuthPageProps) {\n const {branding} = useSettings();\n const isDarkMode = useIsDarkMode();\n const {trans} = useTrans();\n\n return (\n \n \n \n \n
\n {heading &&

{heading}

}\n {children}\n
\n {message &&
{message}
}\n \n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const CheckBoxOutlineBlankIcon = createSvgIcon(\n \n, 'CheckBoxOutlineBlankOutlined');\n","import {createSvgIcon} from '../../../icons/create-svg-icon';\n\nexport const CheckboxFilledIcon = createSvgIcon(\n ,\n 'CheckBox'\n);\n","import {createSvgIcon} from \"../../../icons/create-svg-icon\";\n\nexport const IndeterminateCheckboxFilledIcon = createSvgIcon(\n ,\n 'CheckBox'\n);\n","import React, {\n ChangeEventHandler,\n ComponentPropsWithoutRef,\n ComponentType,\n forwardRef,\n useCallback,\n useEffect,\n} from 'react';\nimport clsx from 'clsx';\nimport {useController} from 'react-hook-form';\nimport {mergeProps, useObjectRef} from '@react-aria/utils';\nimport {useControlledState} from '@react-stately/utils';\nimport {InputSize} from '../input-field/input-size';\nimport {getInputFieldClassNames} from '../input-field/get-input-field-class-names';\nimport {CheckBoxOutlineBlankIcon} from '@common/icons/material/CheckBoxOutlineBlank';\nimport {CheckboxFilledIcon} from './checkbox-filled-icon';\nimport {IndeterminateCheckboxFilledIcon} from './indeterminate-checkbox-filled-icon';\nimport {SvgIconProps} from '@common/icons/svg-icon';\nimport {Orientation} from '../orientation';\nimport {AutoFocusProps, useAutoFocus} from '../../focus/use-auto-focus';\n\nexport interface CheckboxProps\n extends AutoFocusProps,\n Omit, 'size'> {\n size?: InputSize;\n className?: string;\n icon?: React.ComponentType;\n checkedIcon?: React.ComponentType;\n orientation?: Orientation;\n errorMessage?: string;\n isIndeterminate?: boolean;\n invalid?: boolean;\n inputTestId?: string;\n}\nexport const Checkbox = forwardRef(\n (props, ref) => {\n const {\n size = 'md',\n children,\n className,\n icon,\n checkedIcon,\n disabled,\n isIndeterminate,\n errorMessage,\n invalid,\n orientation = 'horizontal',\n onChange,\n autoFocus,\n required,\n value,\n name,\n inputTestId,\n } = props;\n\n const style = getInputFieldClassNames({...props, label: children});\n const Icon = icon || CheckBoxOutlineBlankIcon;\n const CheckedIcon =\n checkedIcon ||\n (isIndeterminate ? IndeterminateCheckboxFilledIcon : CheckboxFilledIcon);\n\n const inputObjRef = useObjectRef(ref);\n useAutoFocus({autoFocus}, inputObjRef);\n\n useEffect(() => {\n // indeterminate is a property, but it can only be set via javascript\n if (inputObjRef.current) {\n inputObjRef.current.indeterminate = isIndeterminate || false;\n }\n });\n\n const [isSelected, setSelected] = useControlledState(\n props.checked,\n props.defaultChecked || false\n );\n\n const updateChecked: ChangeEventHandler = useCallback(\n e => {\n onChange?.(e);\n setSelected(e.target.checked);\n },\n [onChange, setSelected]\n );\n\n const mergedClassName = clsx(\n 'select-none',\n className,\n invalid && 'text-danger',\n !invalid && disabled && 'text-disabled'\n );\n\n let CheckboxIcon: ComponentType;\n let checkboxColor = invalid ? 'text-danger' : null;\n if (isIndeterminate) {\n CheckboxIcon = IndeterminateCheckboxFilledIcon;\n checkboxColor = checkboxColor || 'text-primary';\n } else if (isSelected) {\n CheckboxIcon = CheckedIcon;\n checkboxColor = checkboxColor || 'text-primary';\n } else {\n CheckboxIcon = Icon;\n checkboxColor = checkboxColor || 'text-muted';\n }\n\n // input and icon sizes need to match, as checkbox input is being clicked and not the icon due to pointer-events-none\n return (\n
\n
\n )}\n \n \n {errorMessage &&
{errorMessage}
}\n \n );\n }\n);\n\ninterface FormCheckboxProps extends CheckboxProps {\n name: string;\n}\nexport function FormCheckbox(props: FormCheckboxProps) {\n const {\n field: {onChange, onBlur, value = false, ref},\n fieldState: {invalid, error},\n } = useController({\n name: props.name,\n });\n\n const formProps: Partial = {\n onChange,\n onBlur,\n checked: value,\n invalid,\n errorMessage: error?.message,\n name: props.name,\n };\n\n return ;\n}\n","import {isAbsoluteUrl} from '../urls/is-absolute-url';\n\nclass LazyLoader {\n private loadedAssets: Record> = {};\n\n loadAsset(\n url: string,\n params: {\n id?: string;\n force?: boolean;\n type?: 'js' | 'css';\n parentEl?: HTMLElement;\n } = {type: 'js'}\n ): Promise {\n // script is already loaded, return resolved promise\n if (this.loadedAssets[url] === 'loaded' && !params.force) {\n return new Promise(resolve => resolve());\n }\n\n // script has never been loaded before, load it, return promise and resolve on script load event\n if (\n !this.loadedAssets[url] ||\n (params.force && this.loadedAssets[url] === 'loaded')\n ) {\n this.loadedAssets[url] = new Promise(resolve => {\n const finalUrl = isAbsoluteUrl(url) ? url : `assets/${url}`;\n const finalId = buildId(url, params.id);\n\n if (params.type === 'css') {\n this.loadStyleAsset(finalUrl, finalId, resolve);\n } else {\n this.loadScriptAsset(finalUrl, finalId, resolve, params.parentEl);\n }\n });\n return this.loadedAssets[url] as Promise;\n }\n\n // script is still loading, return existing promise\n return this.loadedAssets[url] as Promise;\n }\n\n /**\n * Check whether asset is loading or has already loaded.\n */\n isLoadingOrLoaded(url: string): boolean {\n return this.loadedAssets[url] != null;\n }\n\n private loadStyleAsset(\n url: string,\n id: string,\n resolve: (value?: any | PromiseLike) => void\n ) {\n const style = document.createElement('link');\n style.rel = 'stylesheet';\n style.id = buildId(url, id);\n style.href = url;\n\n style.onload = () => {\n this.loadedAssets[url] = 'loaded';\n resolve();\n };\n\n document.head.appendChild(style);\n }\n\n private loadScriptAsset(\n url: string,\n id: string,\n resolve: (value?: any | PromiseLike) => void,\n parentEl?: HTMLElement\n ) {\n const s: HTMLScriptElement = document.createElement('script');\n s.async = true;\n s.id = buildId(url, id);\n s.src = url;\n\n s.onload = () => {\n this.loadedAssets[url] = 'loaded';\n resolve();\n };\n\n (parentEl || document.body).appendChild(s);\n }\n}\n\nfunction buildId(url: string, id?: string): string {\n return id || (url.split('/').pop() as string);\n}\n\nexport default new LazyLoader();\n","import lazyLoader from '../utils/http/lazy-loader';\nimport {useCallback, useEffect, useState} from 'react';\nimport {apiClient} from '../http/query-client';\nimport {RecaptchaAction} from '../core/settings/settings';\nimport {toast} from '../ui/toast/toast';\nimport {message} from '../i18n/message';\nimport {useSettings} from '../core/settings/use-settings';\n\nexport function useRecaptcha(action: RecaptchaAction) {\n const {recaptcha: {site_key, enable} = {}} = useSettings();\n const enabled = site_key && enable?.[action];\n\n const [isVerifying, setIsVerifying] = useState(false);\n\n useEffect(() => {\n if (enabled) {\n load(site_key);\n }\n }, [enabled, site_key]);\n\n const verify = useCallback(async () => {\n if (!enabled) return true;\n setIsVerifying(true);\n const isValid = await execute(site_key, action);\n if (!isValid) {\n toast.danger(message('Could not verify you are human.'));\n }\n setIsVerifying(false);\n return isValid;\n }, [enabled, site_key, action]);\n\n return {verify, isVerifying};\n}\n\nasync function execute(siteKey: string, action: string): Promise {\n await load(siteKey);\n return new Promise(resolve => {\n window.grecaptcha?.ready(async () => {\n const token = await window.grecaptcha?.execute(siteKey, {action});\n const result = apiClient\n .post('recaptcha/verify', {token})\n .then(r => r.data.success)\n .catch(() => false);\n resolve(result ?? false);\n });\n });\n}\n\nfunction load(siteKey: string) {\n return lazyLoader.loadAsset(\n `https://www.google.com/recaptcha/api.js?render=${siteKey}`\n );\n}\n","import {Children, memo, ReactElement} from 'react';\nimport {shallowEqual} from '../utils/shallow-equal';\nimport {MetaTag} from './meta-tag';\nimport {TitleMetaTagChildren} from './static-page-title';\nimport {useTrans, UseTransReturn} from '../i18n/use-trans';\nimport {isSsr} from '@common/utils/dom/is-ssr';\n\nconst rafPolyfill = (() => {\n let clock = Date.now();\n\n return (callback: Function) => {\n const currentTime = Date.now();\n\n if (currentTime - clock > 16) {\n clock = currentTime;\n callback(currentTime);\n } else {\n setTimeout(() => {\n rafPolyfill(callback);\n }, 0);\n }\n };\n})();\n\nconst cafPolyfill = (id: string | number) => clearTimeout(id);\n\nconst requestAnimationFrame = !isSsr()\n ? window.requestAnimationFrame\n : global.requestAnimationFrame || rafPolyfill;\n\nconst cancelAnimationFrame = !isSsr()\n ? window.cancelAnimationFrame\n : global.cancelAnimationFrame || cafPolyfill;\n\nexport const helmetAttribute = 'data-be-helmet';\nlet rafId: number | null;\n\ninterface HelmetProps {\n children?: ReactElement | ReactElement[];\n tags?: MetaTag[];\n}\nexport const Helmet = memo(({children, tags}: HelmetProps) => {\n const {trans} = useTrans();\n\n if (isSsr()) return null;\n\n if (!tags && children) {\n tags = mapChildrenToTags(children, trans);\n }\n\n updateTags(tags);\n\n return null;\n}, shallowEqual);\n\nfunction mapChildrenToTags(\n children: ReactElement | ReactElement[],\n trans: UseTransReturn['trans'],\n): MetaTag[] {\n return Children.map(children, child => {\n switch (child.type) {\n case 'title':\n return {\n nodeName: 'title',\n _text: titleTagChildrenToString(child.props.children, trans),\n };\n case 'meta':\n return {...child.props, nodeName: 'meta'};\n }\n });\n}\n\nfunction titleTagChildrenToString(\n children: TitleMetaTagChildren,\n trans: UseTransReturn['trans'],\n): string {\n if (children == null) return '';\n if (typeof children === 'string') return children;\n if (Array.isArray(children)) {\n return children.map(c => titleTagChildrenToString(c, trans)).join('');\n }\n if ('message' in children) {\n return trans(children);\n }\n return trans(children.props);\n}\n\nfunction removeOldTags() {\n if (isSsr()) return;\n document.head\n .querySelectorAll(\n 'meta:not([data-keep]), script meta:not([data-keep]), title, link[rel=\"canonical\"]',\n )\n .forEach(tag => {\n document.head.removeChild(tag);\n });\n}\n\nfunction updateTags(tags?: MetaTag[] | string) {\n if (rafId) {\n cancelAnimationFrame(rafId);\n }\n rafId = requestAnimationFrame(() => {\n removeOldTags();\n\n if (typeof tags === 'string') {\n const template = document.createElement('template');\n template.innerHTML = tags;\n template.content.childNodes.forEach(node => {\n if (node instanceof HTMLElement) {\n node.setAttribute(helmetAttribute, 'true');\n document.head.prepend(node);\n }\n });\n } else {\n tags?.forEach(tag => {\n updateTag(tag);\n });\n }\n\n rafId = null;\n });\n}\n\nfunction updateTag(tag: MetaTag) {\n // update title\n if (tag.nodeName === 'title') {\n if (typeof tag._text !== 'undefined' && document.title !== tag._text) {\n document.title = tag._text;\n }\n return;\n }\n\n // update tag\n const newElement = document.createElement(tag.nodeName);\n for (const key in tag) {\n const attribute = key as keyof MetaTag;\n if (attribute === 'nodeName') continue;\n if (attribute === '_text') {\n newElement.textContent =\n typeof tag._text === 'string' ? tag._text : JSON.stringify(tag._text);\n } else {\n const value = tag[attribute] == null ? '' : tag[attribute];\n newElement.setAttribute(attribute, value as string);\n }\n }\n\n newElement.setAttribute(helmetAttribute, 'true');\n document.head.prepend(newElement);\n}\n","import {Helmet} from './helmet';\nimport {ReactElement} from 'react';\nimport {MessageDescriptor} from '../i18n/message-descriptor';\nimport {useSettings} from '../core/settings/use-settings';\n\ntype TitleChild =\n | string\n | null\n | ReactElement\n | MessageDescriptor;\nexport type TitleMetaTagChildren = TitleChild | TitleChild[];\n\ninterface StaticPageTitleProps {\n children: TitleMetaTagChildren;\n}\nexport function StaticPageTitle({children}: StaticPageTitleProps) {\n const {\n branding: {site_name},\n } = useSettings();\n return (\n \n {children ? (\n // @ts-ignore\n \n {children as any} - {site_name}\n \n ) : undefined}\n \n );\n}\n","import {Link, Navigate, useLocation, useSearchParams} from 'react-router-dom';\nimport {useForm} from 'react-hook-form';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {Button} from '../../ui/buttons/button';\nimport {Form} from '../../ui/forms/form';\nimport {LinkStyle} from '../../ui/buttons/external-link';\nimport {RegisterPayload, useRegister} from '../requests/use-register';\nimport {SocialAuthSection} from './social-auth-section';\nimport {AuthLayout} from './auth-layout/auth-layout';\nimport {Trans} from '../../i18n/trans';\nimport {FormCheckbox} from '../../ui/forms/toggle/checkbox';\nimport {CustomMenuItem} from '../../menus/custom-menu';\nimport {useRecaptcha} from '../../recaptcha/use-recaptcha';\nimport {StaticPageTitle} from '../../seo/static-page-title';\nimport {useSettings} from '../../core/settings/use-settings';\nimport {useContext} from 'react';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\n\nexport function RegisterPage() {\n const {\n branding,\n registration: {disable},\n social,\n } = useSettings();\n const {auth} = useContext(SiteConfigContext);\n const {verify, isVerifying} = useRecaptcha('register');\n\n const {pathname} = useLocation();\n const [searchParams] = useSearchParams();\n\n const isWorkspaceRegister = pathname.includes('workspace');\n const isBillingRegister = searchParams.get('redirectFrom') === 'pricing';\n const searchParamsEmail = searchParams.get('email') || undefined;\n\n const form = useForm({\n defaultValues: {email: searchParamsEmail},\n });\n const register = useRegister(form);\n\n if (disable) {\n return ;\n }\n\n let heading = ;\n if (isWorkspaceRegister) {\n heading = (\n \n );\n } else if (isBillingRegister) {\n heading = ;\n }\n\n const message = (\n (\n \n {parts}\n \n ),\n }}\n message=\"Already have an account? Sign in.\"\n />\n );\n\n return (\n \n \n \n \n {\n const isValid = await verify();\n if (isValid) {\n register.mutate(payload);\n }\n }}\n >\n }\n required\n />\n }\n required\n />\n }\n required\n />\n {auth?.registerFields ? : null}\n \n \n \n \n \n ) : (\n \n )\n }\n />\n \n \n );\n}\n\nfunction PolicyCheckboxes() {\n const {\n registration: {policies},\n } = useSettings();\n\n if (!policies) return null;\n\n return (\n
\n {policies.map(policy => (\n \n LinkStyle}\n item={policy}\n />\n ),\n }}\n />\n \n ))}\n
\n );\n}\n","import {useQuery} from '@tanstack/react-query';\nimport {apiClient} from '../http/query-client';\nimport {BackendResponse} from '../http/backend-response/backend-response';\nimport {CustomPage} from '../admin/custom-pages/custom-page';\nimport {useParams} from 'react-router-dom';\nimport {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';\n\nconst endpoint = (slugOrId: number | string) => `custom-pages/${slugOrId}`;\n\nexport interface FetchCustomPageResponse extends BackendResponse {\n page: CustomPage;\n}\n\nexport function useCustomPage(pageId?: number | string) {\n const params = useParams();\n if (!pageId) {\n pageId = params.pageId;\n }\n return useQuery({\n queryKey: [endpoint(pageId!)],\n queryFn: () => fetchCustomPage(pageId!),\n initialData: () => {\n const data = getBootstrapData().loaders?.customPage;\n if (data?.page && (data.page.id == pageId || data.page.slug == pageId)) {\n return data;\n }\n },\n });\n}\n\nfunction fetchCustomPage(\n slugOrId: number | string,\n): Promise {\n return apiClient.get(endpoint(slugOrId)).then(response => response.data);\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const NotificationsIcon = createSvgIcon(\n \n, 'NotificationsOutlined');\n","import {ReactNode} from 'react';\nimport clsx from 'clsx';\n\nexport interface BadgeProps {\n children: ReactNode;\n className?: string;\n withBorder?: boolean;\n top?: string;\n right?: string;\n}\nexport function Badge({\n children,\n className,\n withBorder = true,\n top = 'top-2',\n right = 'right-4',\n}: BadgeProps) {\n return (\n \n {children}\n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const DoneAllIcon = createSvgIcon(\n ,\n 'DoneAllOutlined'\n);\n","import React, {ReactNode} from 'react';\nimport clsx from 'clsx';\nimport {InputSize} from '../forms/input-field/input-size';\n\nexport interface IllustratedMessageProps {\n className?: string;\n size?: InputSize;\n image?: ReactNode;\n imageHeight?: string;\n imageMargin?: string;\n title?: ReactNode;\n description?: ReactNode;\n action?: ReactNode;\n}\nexport function IllustratedMessage({\n image,\n title,\n description,\n action,\n className,\n size = 'md',\n imageHeight,\n imageMargin = 'mb-24',\n}: IllustratedMessageProps) {\n const style = getSizeClassName(size, imageHeight);\n return (\n
\n {image &&
{image}
}\n {title && (\n
{title}
\n )}\n {description && (\n
\n {description}\n
\n )}\n {action &&
{action}
}\n
\n );\n}\n\nfunction getSizeClassName(size: InputSize, imageHeight?: string) {\n switch (size) {\n case 'xs':\n return {\n image: imageHeight || 'h-60',\n title: 'text-sm',\n description: 'text-xs',\n };\n case 'sm':\n return {\n image: imageHeight || 'h-80',\n title: 'text-base',\n description: 'text-sm',\n };\n default:\n return {\n image: imageHeight || 'h-128',\n title: 'text-lg',\n description: 'text-base',\n };\n }\n}\n","export default \"__VITE_ASSET__534ff342__\"","import {IllustratedMessage} from '../../ui/images/illustrated-message';\nimport {SvgImage} from '../../ui/images/svg-image/svg-image';\nimport notifySvg from './notify.svg';\nimport {Trans} from '../../i18n/trans';\nimport {Button} from '../../ui/buttons/button';\nimport {Link} from 'react-router-dom';\nimport {useSettings} from '../../core/settings/use-settings';\n\nexport function NotificationEmptyStateMessage() {\n const {notif} = useSettings();\n return (\n }\n title={}\n description={\n \n }\n action={\n notif.subs.integrated && (\n \n \n \n )\n }\n />\n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const SettingsIcon = createSvgIcon(\n \n, 'SettingsOutlined');\n","import {IconButton} from '../../ui/buttons/icon-button';\nimport {NotificationsIcon} from '../../icons/material/Notifications';\nimport {Button} from '../../ui/buttons/button';\nimport {useUserNotifications} from './requests/user-notifications';\nimport {ProgressCircle} from '../../ui/progress/progress-circle';\nimport {NotificationList} from '../notification-list';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {Trans} from '../../i18n/trans';\nimport {useAuth} from '../../auth/use-auth';\nimport {Badge} from '../../ui/badge/badge';\nimport {DoneAllIcon} from '../../icons/material/DoneAll';\nimport {useMarkNotificationsAsRead} from '../requests/use-mark-notifications-as-read';\nimport {NotificationEmptyStateMessage} from '../empty-state/notification-empty-state-message';\nimport {SettingsIcon} from '@common/icons/material/Settings';\nimport {Link} from 'react-router-dom';\n\ninterface NotificationDialogTriggerProps {\n className?: string;\n}\nexport function NotificationDialogTrigger({\n className,\n}: NotificationDialogTriggerProps) {\n const {user} = useAuth();\n const query = useUserNotifications();\n const markAsRead = useMarkNotificationsAsRead();\n const hasUnread = !!user?.unread_notifications_count;\n\n const handleMarkAsRead = () => {\n if (!query.data) return;\n markAsRead.mutate({\n ids: query.data.pagination.data.map(n => n.id),\n });\n };\n\n return (\n \n \n {user?.unread_notifications_count}\n \n ) : undefined\n }\n >\n \n \n \n \n \n \n )\n }\n rightAdornment={\n hasUnread && (\n }\n onClick={handleMarkAsRead}\n disabled={markAsRead.isPending}\n className=\"max-md:hidden\"\n >\n \n \n )\n }\n >\n \n \n \n \n \n \n \n );\n}\n\nfunction DialogContent() {\n const {data, isLoading} = useUserNotifications();\n if (isLoading) {\n return (\n
\n \n
\n );\n }\n if (!data?.pagination.data.length) {\n return (\n
\n \n
\n );\n }\n return (\n
\n \n
\n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const MenuIcon = createSvgIcon(\n \n, 'MenuOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const PersonIcon = createSvgIcon(\n \n, 'PersonOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const ArrowDropDownIcon = createSvgIcon(\n \n, 'ArrowDropDownOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const PaymentsIcon = createSvgIcon(\n \n, 'PaymentsOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const AccountCircleIcon = createSvgIcon(\n \n, 'AccountCircleOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const DarkModeIcon = createSvgIcon(\n \n, 'DarkModeOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const LightModeIcon = createSvgIcon(\n \n, 'LightModeOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const ExitToAppIcon = createSvgIcon(\n \n, 'ExitToAppOutlined');\n","import {ReactElement, useContext} from 'react';\nimport {ListboxItemProps} from '@common/ui/forms/listbox/item';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\nimport {useLogout} from '@common/auth/requests/logout';\nimport {useCustomMenu} from '@common/menus/use-custom-menu';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {useAuth} from '@common/auth/use-auth';\nimport {useNavigate} from '@common/utils/hooks/use-navigate';\nimport {useThemeSelector} from '@common/ui/themes/theme-selector-context';\nimport {\n Menu,\n MenuItem,\n MenuTrigger,\n} from '@common/ui/navigation/menu/menu-trigger';\nimport {NotificationsIcon} from '@common/icons/material/Notifications';\nimport {Trans} from '@common/i18n/trans';\nimport {PaymentsIcon} from '@common/icons/material/Payments';\nimport {createSvgIconFromTree} from '@common/icons/create-svg-icon';\nimport {AccountCircleIcon} from '@common/icons/material/AccountCircle';\nimport {DarkModeIcon} from '@common/icons/material/DarkMode';\nimport {LightModeIcon} from '@common/icons/material/LightMode';\nimport {ExitToAppIcon} from '@common/icons/material/ExitToApp';\n\ninterface Props {\n children: ReactElement;\n items?: ReactElement[];\n}\nexport function NavbarAuthMenu({children, items}: Props) {\n const {auth} = useContext(SiteConfigContext);\n const logout = useLogout();\n const menu = useCustomMenu('auth-dropdown');\n const {notifications, themes} = useSettings();\n const {user, isSubscribed} = useAuth();\n const navigate = useNavigate();\n const {selectedTheme, selectTheme} = useThemeSelector();\n if (!selectedTheme || !user) return null;\n const hasUnreadNotif = !!user.unread_notifications_count;\n\n const notifMenuItem = (\n }\n onSelected={() => {\n navigate('/notifications');\n }}\n >\n \n {hasUnreadNotif ? ` (${user.unread_notifications_count})` : undefined}\n \n );\n\n const billingMenuItem = (\n }\n onSelected={() => {\n navigate('/billing');\n }}\n >\n \n \n );\n\n return (\n \n {children}\n \n {menu &&\n menu.items.map(item => {\n const Icon = item.icon && createSvgIconFromTree(item.icon);\n return (\n }\n onSelected={() => {\n if (item.type === 'link') {\n window.open(item.action, '_blank');\n } else {\n navigate(item.action);\n }\n }}\n >\n \n \n );\n })}\n {auth.getUserProfileLink && (\n }\n onSelected={() => {\n navigate(auth.getUserProfileLink!(user));\n }}\n >\n \n \n )}\n {items?.map(item => item)}\n {notifications?.integrated ? notifMenuItem : undefined}\n {isSubscribed && billingMenuItem}\n {themes?.user_change && !selectedTheme.is_dark && (\n }\n onSelected={() => {\n selectTheme('dark');\n }}\n >\n \n \n )}\n {themes?.user_change && selectedTheme.is_dark && (\n }\n onSelected={() => {\n selectTheme('light');\n }}\n >\n \n \n )}\n }\n onSelected={() => {\n logout.mutate();\n }}\n >\n \n \n \n \n );\n}\n","import {useAuth} from '@common/auth/use-auth';\nimport {useThemeSelector} from '@common/ui/themes/theme-selector-context';\nimport {Badge} from '@common/ui/badge/badge';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {PersonIcon} from '@common/icons/material/Person';\nimport {ButtonBase} from '@common/ui/buttons/button-base';\nimport {ArrowDropDownIcon} from '@common/icons/material/ArrowDropDown';\nimport {ReactElement} from 'react';\nimport {ListboxItemProps} from '@common/ui/forms/listbox/item';\nimport {NavbarAuthMenu} from '@common/ui/navigation/navbar/navbar-auth-menu';\n\nexport interface NavbarAuthUserProps {\n items?: ReactElement[];\n}\nexport function NavbarAuthUser({items = []}: NavbarAuthUserProps) {\n const {user} = useAuth();\n const {selectedTheme} = useThemeSelector();\n if (!selectedTheme || !user) return null;\n const hasUnreadNotif = !!user.unread_notifications_count;\n\n const mobileButton = (\n {user.unread_notifications_count}\n ) : undefined\n }\n >\n \n \n );\n const desktopButton = (\n \n \n \n {user.display_name}\n \n \n \n );\n\n return (\n \n \n {mobileButton}\n {desktopButton}\n \n \n );\n}\n","import {ButtonColor} from '@common/ui/buttons/get-shared-button-style';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {useNavigate} from '@common/utils/hooks/use-navigate';\nimport {Menu, MenuTrigger} from '@common/ui/navigation/menu/menu-trigger';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {PersonIcon} from '@common/icons/material/Person';\nimport {Item} from '@common/ui/forms/listbox/item';\nimport {Trans} from '@common/i18n/trans';\nimport {Link} from 'react-router-dom';\nimport {Button} from '@common/ui/buttons/button';\nimport {NavbarProps} from '@common/ui/navigation/navbar/navbar';\nimport {Fragment} from 'react';\n\ninterface NavbarAuthButtonsProps {\n primaryButtonColor?: ButtonColor;\n navbarColor?: NavbarProps['color'];\n}\nexport function NavbarAuthButtons({\n primaryButtonColor,\n navbarColor,\n}: NavbarAuthButtonsProps) {\n if (!primaryButtonColor) {\n primaryButtonColor = navbarColor === 'primary' ? 'paper' : 'primary';\n }\n\n return (\n \n \n \n \n );\n}\n\ninterface DesktopButtonsProps {\n primaryButtonColor: ButtonColor;\n}\nfunction DesktopButtons({primaryButtonColor}: DesktopButtonsProps) {\n const {registration} = useSettings();\n return (\n
\n {!registration.disable && (\n \n \n \n )}\n \n \n \n
\n );\n}\n\nfunction MobileButtons() {\n const {registration} = useSettings();\n const navigate = useNavigate();\n return (\n \n \n \n \n \n navigate('/login')}>\n \n \n {!registration.disable && (\n navigate('/register')}>\n \n \n )}\n \n \n );\n}\n","import {useBootstrapData} from '@common/core/bootstrap-data/bootstrap-data-context';\nimport {useIsDarkMode} from '@common/ui/themes/use-is-dark-mode';\n\nexport function useDarkThemeVariables() {\n const {data} = useBootstrapData();\n const isDarkMode = useIsDarkMode();\n // already in dark mode, no need to set variables again\n if (isDarkMode) {\n return undefined;\n }\n return data.themes.all.find(theme => theme.is_dark && theme.default_dark)\n ?.colors;\n}\n","import {useTrans} from '@common/i18n/use-trans';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {Link} from 'react-router-dom';\nimport {NavbarProps} from '@common/ui/navigation/navbar/navbar';\n\ninterface LogoProps {\n color?: NavbarProps['color'];\n logoColor?: NavbarProps['logoColor'];\n isDarkMode?: boolean;\n}\nexport function Logo({color, logoColor, isDarkMode}: LogoProps) {\n const {trans} = useTrans();\n const {branding} = useSettings();\n\n let desktopLogo: string;\n let mobileLogo: string;\n if (\n isDarkMode ||\n !branding.logo_dark ||\n (logoColor !== 'dark' && color !== 'bg' && color !== 'bg-alt')\n ) {\n desktopLogo = branding.logo_light;\n mobileLogo = branding.logo_light_mobile;\n } else {\n desktopLogo = branding.logo_dark;\n mobileLogo = branding.logo_dark_mobile;\n }\n\n if (!mobileLogo && !desktopLogo) {\n return null;\n }\n\n return (\n \n \n \n \n \n \n \n );\n}\n","import {ReactElement, ReactNode} from 'react';\nimport clsx from 'clsx';\nimport {useAuth} from '@common/auth/use-auth';\nimport {NotificationDialogTrigger} from '@common/notifications/dialog/notification-dialog-trigger';\nimport {Menu, MenuTrigger} from '@common/ui/navigation/menu/menu-trigger';\nimport {useCustomMenu} from '@common/menus/use-custom-menu';\nimport {createSvgIconFromTree} from '@common/icons/create-svg-icon';\nimport {Trans} from '@common/i18n/trans';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {Item} from '@common/ui/forms/listbox/item';\nimport {useNavigate} from '@common/utils/hooks/use-navigate';\nimport {useIsDarkMode} from '@common/ui/themes/use-is-dark-mode';\nimport {CustomMenu} from '@common/menus/custom-menu';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {ButtonColor} from '@common/ui/buttons/get-shared-button-style';\nimport {MenuIcon} from '@common/icons/material/Menu';\nimport {MenuItemConfig} from '@common/core/settings/settings';\nimport {\n NavbarAuthUser,\n NavbarAuthUserProps,\n} from '@common/ui/navigation/navbar/navbar-auth-user';\nimport {NavbarAuthButtons} from '@common/ui/navigation/navbar/navbar-auth-buttons';\nimport {useDarkThemeVariables} from '@common/ui/themes/use-dark-theme-variables';\nimport {Logo} from '@common/ui/navigation/navbar/logo';\n\ntype NavbarColor = 'primary' | 'bg' | 'bg-alt' | 'transparent' | string;\n\nexport interface NavbarProps {\n hideLogo?: boolean | null;\n toggleButton?: ReactElement;\n children?: ReactNode;\n className?: string;\n color?: NavbarColor;\n bgOpacity?: number | string;\n darkModeColor?: NavbarColor;\n logoColor?: 'dark' | 'light';\n textColor?: string;\n primaryButtonColor?: ButtonColor;\n border?: string;\n size?: 'xs' | 'sm' | 'md';\n rightChildren?: ReactNode;\n menuPosition?: string;\n authMenuItems?: NavbarAuthUserProps['items'];\n alwaysDarkMode?: boolean;\n wrapInContainer?: boolean;\n}\nexport function Navbar(props: NavbarProps) {\n let {\n hideLogo,\n toggleButton,\n children,\n className,\n border,\n size = 'md',\n color = 'primary',\n textColor,\n darkModeColor = 'bg-alt',\n rightChildren,\n menuPosition,\n logoColor,\n primaryButtonColor,\n authMenuItems,\n alwaysDarkMode = false,\n wrapInContainer = false,\n } = props;\n const isDarkMode = useIsDarkMode() || alwaysDarkMode;\n const {notifications} = useSettings();\n const {isLoggedIn} = useAuth();\n\n const darkThemeVars = useDarkThemeVariables();\n\n const showNotifButton = isLoggedIn && notifications?.integrated;\n\n if (isDarkMode) {\n color = darkModeColor;\n }\n\n return (\n \n \n {!hideLogo && (\n \n )}\n {toggleButton}\n {children}\n \n \n
\n {rightChildren}\n {showNotifButton && }\n {isLoggedIn ? (\n \n ) : (\n \n )}\n
\n \n \n );\n}\n\ninterface DesktopMenuProps {\n position: NavbarProps['menuPosition'];\n}\nfunction DesktopMenu({position}: DesktopMenuProps) {\n return (\n \n clsx(\n 'opacity-90 hover:underline hover:opacity-100',\n isActive && 'opacity-100',\n )\n }\n menu={position}\n />\n );\n}\n\ninterface MobileMenuProps {\n position: NavbarProps['menuPosition'];\n}\nfunction MobileMenu({position}: MobileMenuProps) {\n const navigate = useNavigate();\n const menu = useCustomMenu(position);\n\n if (!menu?.items.length) {\n return null;\n }\n\n const handleItemClick = (item: MenuItemConfig) => {\n if (item.type === 'route') {\n navigate(item.action);\n } else {\n window.open(item.action, item.target)?.focus();\n }\n };\n\n return (\n \n \n \n \n \n {menu.items.map(item => {\n const Icon = item.icon && createSvgIconFromTree(item.icon);\n return (\n handleItemClick(item)}\n key={item.id}\n startIcon={Icon && }\n >\n \n \n );\n })}\n \n \n );\n}\n\nfunction getColorStyle(color: string, textColor?: string): string {\n switch (color) {\n case 'primary':\n return `bg-primary ${textColor || 'text-on-primary'} border-b-primary`;\n case 'bg':\n return `bg ${textColor || 'text-main'} border-b`;\n case 'bg-alt':\n return `bg-alt ${textColor || 'text-main'} border-b`;\n case 'transparent':\n return `bg-transparent ${textColor || 'text-white'}`;\n default:\n return `${color} ${textColor}`;\n }\n}\n","import {keepPreviousData, useQuery} from '@tanstack/react-query';\nimport {BackendResponse} from './backend-response/backend-response';\nimport {Localization} from '../i18n/localization';\nimport {CssTheme} from '../ui/themes/css-theme';\nimport {Role} from '../auth/role';\nimport {Permission} from '../auth/permission';\nimport {apiClient, queryClient} from './query-client';\nimport {MenuItemCategory} from '../admin/appearance/sections/menus/menu-item-category';\nimport {CustomPage} from '../admin/custom-pages/custom-page';\nimport {CustomDomain} from '../custom-domains/custom-domain';\nimport {MessageDescriptor} from '@common/i18n/message-descriptor';\n\nexport interface FetchValueListsResponse extends BackendResponse {\n countries?: CountryListItem[];\n timezones?: {[key: string]: Timezone[]};\n languages?: LanguageListItem[];\n localizations?: Localization[];\n currencies?: {[key: string]: Currency};\n domains?: CustomDomain[];\n pages?: CustomPage[];\n themes?: CssTheme[];\n permissions?: Permission[];\n workspacePermissions?: Permission[];\n roles?: Role[];\n menuItemCategories?: MenuItemCategory[];\n googleFonts?: FontConfig[];\n workspaceRoles?: Role[];\n}\n\nexport interface CountryListItem {\n name: string;\n code: string;\n}\n\nexport interface LanguageListItem {\n name: string;\n nativeName?: string;\n code: string;\n}\n\nexport interface Currency {\n name: string;\n decimal_digits: number;\n symbol: string;\n code: string;\n}\n\nexport interface Timezone {\n text: string;\n value: string;\n}\n\nexport interface FontConfig {\n label?: MessageDescriptor;\n family: string;\n category?: string;\n google?: boolean;\n}\n\ninterface Options {\n disabled?: boolean;\n}\n\nexport function useValueLists(\n names: (keyof FetchValueListsResponse)[],\n params?: Record,\n options: Options = {},\n) {\n return useQuery({\n queryKey: ['value-lists', names, params],\n queryFn: () => fetchValueLists(names, params),\n // if there are params, make sure we update lists when they change\n staleTime: !params ? Infinity : undefined,\n placeholderData: keepPreviousData,\n enabled: !options.disabled,\n initialData: () => {\n // check if we have already fetched value lists for all specified names previously,\n // if so, return cached response for this query, as there's no need to fetch it again\n const previousData = queryClient\n .getQueriesData({queryKey: ['ValueLists']})\n .find(([, response]) => {\n if (response && names.every(n => response[n])) {\n return response;\n }\n return null;\n });\n if (previousData) {\n return previousData[1];\n }\n },\n });\n}\n\nexport function prefetchValueLists(\n names: (keyof FetchValueListsResponse)[],\n params?: Record,\n) {\n queryClient.prefetchQuery({\n queryKey: ['value-lists', names, params],\n queryFn: () => fetchValueLists(names, params),\n });\n}\n\nfunction fetchValueLists(\n names: (keyof FetchValueListsResponse)[],\n params?: Record,\n): Promise {\n return apiClient\n .get(`value-lists/${names}`, {params})\n .then(response => response.data);\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const LanguageIcon = createSvgIcon(\n \n, 'LanguageOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const KeyboardArrowDownIcon = createSvgIcon(\n \n, 'KeyboardArrowDownOutlined');\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../http/backend-response/backend-response';\nimport {Localization} from './localization';\nimport {apiClient} from '../http/query-client';\nimport {showHttpErrorToast} from '../utils/http/show-http-error-toast';\nimport {useBootstrapData} from '../core/bootstrap-data/bootstrap-data-context';\n\nexport interface ChangeLocaleResponse extends BackendResponse {\n locale: Localization;\n}\n\nexport function useChangeLocale() {\n const {mergeBootstrapData} = useBootstrapData();\n return useMutation({\n mutationFn: (props: {locale?: string}) => changeLocale(props),\n onSuccess: response => {\n mergeBootstrapData({\n i18n: response.locale,\n });\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction changeLocale(props: {locale?: string}): Promise {\n return apiClient.post(`users/me/locale`, props).then(r => r.data);\n}\n","import {useValueLists} from '../http/value-lists';\nimport {Button} from '../ui/buttons/button';\nimport {LanguageIcon} from '../icons/material/Language';\nimport {KeyboardArrowDownIcon} from '../icons/material/KeyboardArrowDown';\nimport {useSelectedLocale} from './selected-locale';\nimport {Menu, MenuItem, MenuTrigger} from '../ui/navigation/menu/menu-trigger';\nimport {useChangeLocale} from './change-locale';\nimport {useSettings} from '../core/settings/use-settings';\n\nexport function LocaleSwitcher() {\n const {locale} = useSelectedLocale();\n const changeLocale = useChangeLocale();\n const {data} = useValueLists(['localizations']);\n const {i18n} = useSettings();\n\n if (!data?.localizations || !locale || !i18n.enable) return null;\n\n return (\n {\n const newLocale = value as string;\n if (newLocale !== locale?.language) {\n changeLocale.mutate({locale: newLocale});\n }\n }}\n >\n }\n endIcon={}\n >\n {locale.name}\n \n \n {data.localizations.map(localization => (\n \n {localization.name}\n \n ))}\n \n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const LightbulbIcon = createSvgIcon(\n \n, 'LightbulbOutlined');\n","import clsx from 'clsx';\nimport {CustomMenu} from '../../menus/custom-menu';\nimport {LocaleSwitcher} from '../../i18n/locale-switcher';\nimport {Button} from '../buttons/button';\nimport {DarkModeIcon} from '../../icons/material/DarkMode';\nimport {LightbulbIcon} from '../../icons/material/Lightbulb';\nimport {Trans} from '../../i18n/trans';\nimport {useThemeSelector} from '../themes/theme-selector-context';\nimport {useSettings} from '../../core/settings/use-settings';\n\ninterface Props {\n className?: string;\n padding?: string;\n}\n\nexport function Footer({className, padding}: Props) {\n const year = new Date().getFullYear();\n const {branding} = useSettings();\n return (\n \n \n
\n \n
\n \n \n
\n
\n \n );\n}\n\nfunction Menus() {\n const settings = useSettings();\n const primaryMenu = settings.menus.find(m => m.positions?.includes('footer'));\n const secondaryMenu = settings.menus.find(\n m => m.positions?.includes('footer-secondary'),\n );\n\n if (!primaryMenu && !secondaryMenu) return null;\n\n return (\n
\n {primaryMenu && (\n \n )}\n {secondaryMenu && (\n \n )}\n
\n );\n}\n\nfunction ThemeSwitcher() {\n const {themes} = useSettings();\n const {selectedTheme, selectTheme} = useThemeSelector();\n if (!selectedTheme || !themes?.user_change) return null;\n\n return (\n : }\n onClick={() => {\n if (selectedTheme.is_dark) {\n selectTheme('light');\n } else {\n selectTheme('dark');\n }\n }}\n >\n {selectedTheme.is_dark ? (\n \n ) : (\n \n )}\n \n );\n}\n","export function highlightCode(el: HTMLElement) {\n import('@common/text-editor/highlight/highlight').then(({hljs}) => {\n el.querySelectorAll('pre code').forEach(block => {\n hljs.highlightElement(block as HTMLElement);\n });\n });\n}\n","import {CustomPage} from '@common/admin/custom-pages/custom-page';\nimport {useEffect, useRef} from 'react';\nimport {highlightCode} from '@common/text-editor/highlight/highlight-code';\n\ninterface CustomPageBodyProps {\n page: CustomPage;\n}\nexport function CustomPageBody({page}: CustomPageBodyProps) {\n const bodyRef = useRef(null);\n useEffect(() => {\n if (bodyRef.current) {\n highlightCode(bodyRef.current);\n }\n }, []);\n\n return (\n
\n
\n

{page.title}

\n \n
\n
\n );\n}\n","import {Helmet} from './helmet';\nimport {useBootstrapData} from '../core/bootstrap-data/bootstrap-data-context';\n\nexport function DefaultMetaTags() {\n const {\n data: {default_meta_tags},\n } = useBootstrapData();\n return ;\n}\n","import {UseQueryResult} from '@tanstack/react-query';\nimport {Helmet} from '@common/seo/helmet';\nimport {DefaultMetaTags} from '@common/seo/default-meta-tags';\nimport React from 'react';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\n\ninterface Props {\n query: UseQueryResult;\n}\nexport function PageMetaTags({query}: Props) {\n if (query.data?.set_seo) {\n return null;\n }\n return query.data?.seo ? (\n \n ) : (\n \n );\n}\n","import {ProgressCircle} from './progress-circle';\nimport React from 'react';\nimport clsx from 'clsx';\n\ninterface FullPageLoaderProps {\n className?: string;\n screen?: boolean;\n}\nexport function FullPageLoader({className, screen}: FullPageLoaderProps) {\n return (\n \n \n \n );\n}\n","export default \"__VITE_ASSET__5953daae__\"","export default \"__VITE_ASSET__096c5f85__\"","import {Trans} from '../../i18n/trans';\nimport {Button} from '../buttons/button';\nimport {Link} from 'react-router-dom';\nimport imgUrl1 from './404-1.png';\nimport imgUrl2 from './404-2.png';\n\nexport function NotFoundPage() {\n return (\n
\n
\n
\n
\n
\n

\n \n

\n

\n \n

\n \n \n \n
\n
\n
\n \"\"\n
\n
\n
\n
\n \"\"\n
\n
\n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const ErrorIcon = createSvgIcon(\n \n, 'ErrorOutlined');\n","import {IllustratedMessage} from '@common/ui/images/illustrated-message';\nimport {ErrorIcon} from '@common/icons/material/Error';\nimport {Trans} from '@common/i18n/trans';\n\nexport function PageErrorMessage() {\n return (\n \n \n \n }\n imageHeight=\"h-auto\"\n title={}\n description={}\n />\n );\n}\n","import {useEffect, useRef, useState} from 'react';\n\ninterface SpinDelayOptions {\n delay?: number;\n minDuration?: number;\n}\n\ntype State = 'IDLE' | 'DELAY' | 'DISPLAY' | 'EXPIRE';\n\nexport const defaultOptions = {\n delay: 500,\n minDuration: 200,\n};\n\nexport function useSpinDelay(\n loading: boolean,\n options?: SpinDelayOptions,\n): boolean {\n options = Object.assign({}, defaultOptions, options);\n\n const [state, setState] = useState('IDLE');\n const timeout = useRef(null);\n\n useEffect(() => {\n if (loading && state === 'IDLE') {\n clearTimeout(timeout.current);\n\n timeout.current = setTimeout(\n () => {\n if (!loading) {\n return setState('IDLE');\n }\n\n timeout.current = setTimeout(\n () => {\n setState('EXPIRE');\n },\n options?.minDuration,\n );\n\n setState('DISPLAY');\n },\n options?.delay,\n );\n\n setState('DELAY');\n }\n\n if (!loading && state !== 'DISPLAY') {\n clearTimeout(timeout.current);\n setState('IDLE');\n }\n }, [loading, state, options.delay, options.minDuration]);\n\n useEffect(() => {\n return () => clearTimeout(timeout.current);\n }, []);\n\n return state === 'DISPLAY' || state === 'EXPIRE';\n}\n\nexport default useSpinDelay;\n","import {FullPageLoader} from '@common/ui/progress/full-page-loader';\nimport {errorStatusIs} from '@common/utils/http/error-status-is';\nimport {NotFoundPage} from '@common/ui/not-found-page/not-found-page';\nimport {PageErrorMessage} from '@common/errors/page-error-message';\nimport React, {ReactNode} from 'react';\nimport {UseQueryResult} from '@tanstack/react-query';\nimport {Navigate} from 'react-router-dom';\nimport {useAuth} from '@common/auth/use-auth';\nimport useSpinDelay from '@common/utils/hooks/use-spin-delay';\n\ninterface Props {\n query: UseQueryResult;\n show404?: boolean;\n loaderClassName?: string;\n loaderIsScreen?: boolean;\n loader?: ReactNode;\n delayedSpinner?: boolean;\n}\nexport function PageStatus({\n query,\n show404 = true,\n loader,\n loaderClassName,\n loaderIsScreen = true,\n delayedSpinner = true,\n}: Props) {\n const {isLoggedIn} = useAuth();\n\n const showSpinner = useSpinDelay(query.isLoading, {\n delay: 500,\n minDuration: 200,\n });\n\n if (query.isLoading) {\n if (!showSpinner && delayedSpinner) {\n return null;\n }\n return (\n loader || (\n \n )\n );\n }\n\n if (\n query.isError &&\n (errorStatusIs(query.error, 401) || errorStatusIs(query.error, 403)) &&\n !isLoggedIn\n ) {\n return ;\n }\n\n if (show404 && query.isError && errorStatusIs(query.error, 404)) {\n return ;\n }\n\n return ;\n}\n","import {useParams} from 'react-router-dom';\nimport {useCustomPage} from './use-custom-page';\nimport {Navbar} from '../ui/navigation/navbar/navbar';\nimport {Footer} from '../ui/footer/footer';\nimport {CustomPageBody} from '@common/custom-page/custom-page-body';\nimport {PageMetaTags} from '@common/http/page-meta-tags';\nimport {PageStatus} from '@common/http/page-status';\nimport {useEffect} from 'react';\n\ninterface Props {\n slug?: string;\n}\nexport function CustomPageLayout({slug}: Props) {\n const {pageSlug} = useParams();\n const query = useCustomPage(slug || pageSlug!);\n\n useEffect(() => {\n if (query.data?.page) {\n window.scrollTo(0, 0);\n }\n }, [query]);\n\n return (\n
\n \n \n
\n {query.data ? (\n \n ) : (\n \n )}\n
\n
\n
\n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {onFormQueryError} from '../../errors/on-form-query-error';\nimport {useNavigate} from '../../utils/hooks/use-navigate';\nimport {apiClient} from '../../http/query-client';\nimport {useAuth} from '../use-auth';\nimport {useBootstrapData} from '../../core/bootstrap-data/bootstrap-data-context';\nimport {useCallback} from 'react';\n\ninterface LoginResponse extends BackendResponse {\n bootstrapData: string;\n two_factor: false;\n}\ninterface TwoFactorResponse {\n two_factor: true;\n}\n\ntype Response = LoginResponse | TwoFactorResponse;\n\nexport interface LoginPayload {\n email: string;\n password: string;\n remember: boolean;\n token_name: string;\n}\n\nexport function useLogin(form: UseFormReturn) {\n const handleSuccess = useHandleLoginSuccess();\n return useMutation({\n mutationFn: login,\n onSuccess: response => {\n if (!response.two_factor) {\n handleSuccess(response);\n }\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nexport function useHandleLoginSuccess() {\n const navigate = useNavigate();\n const {getRedirectUri} = useAuth();\n const {setBootstrapData} = useBootstrapData();\n\n return useCallback(\n (response: LoginResponse) => {\n setBootstrapData(response.bootstrapData);\n navigate(getRedirectUri(), {replace: true});\n },\n [navigate, setBootstrapData, getRedirectUri],\n );\n}\n\nfunction login(payload: LoginPayload): Promise {\n return apiClient.post('auth/login', payload).then(response => response.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\nimport {UseFormReturn} from 'react-hook-form';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {useHandleLoginSuccess} from '@common/auth/requests/use-login';\n\ninterface Response extends BackendResponse {\n bootstrapData: string;\n two_factor: false;\n}\n\nexport interface TwoFactorChallengePayload {\n code?: string;\n recovery_code?: string;\n}\nexport function useTwoFactorChallenge(\n form: UseFormReturn,\n) {\n const handleSuccess = useHandleLoginSuccess();\n return useMutation({\n mutationFn: (payload: TwoFactorChallengePayload) =>\n completeChallenge(payload),\n onSuccess: response => {\n handleSuccess(response);\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction completeChallenge(\n payload: TwoFactorChallengePayload,\n): Promise {\n return apiClient\n .post('auth/two-factor-challenge', payload)\n .then(response => response.data);\n}\n","import {useForm} from 'react-hook-form';\nimport {FormTextField} from '../../../ui/forms/input-field/text-field/text-field';\nimport {Button} from '../../../ui/buttons/button';\nimport {Form} from '../../../ui/forms/form';\nimport {AuthLayout} from '../auth-layout/auth-layout';\nimport {Trans} from '../../../i18n/trans';\nimport {StaticPageTitle} from '../../../seo/static-page-title';\nimport {\n TwoFactorChallengePayload,\n useTwoFactorChallenge,\n} from '@common/auth/ui/two-factor/requests/use-two-factor-challenge';\nimport {useState} from 'react';\n\nexport function TwoFactorChallengePage() {\n const [usingRecoveryCode, setUsingRecoveryCode] = useState(false);\n\n const form = useForm();\n const completeChallenge = useTwoFactorChallenge(form);\n\n return (\n \n \n \n \n {\n completeChallenge.mutate(payload);\n }}\n >\n
\n \n
\n
\n {usingRecoveryCode ? (\n }\n autoFocus\n required\n />\n ) : (\n }\n autoFocus\n required\n />\n )}\n
\n
\n setUsingRecoveryCode(!usingRecoveryCode)}\n >\n \n \n
\n \n \n \n \n
\n );\n}\n","import {Link, useLocation, useSearchParams} from 'react-router-dom';\nimport {useForm} from 'react-hook-form';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {Button} from '../../ui/buttons/button';\nimport {Form} from '../../ui/forms/form';\nimport {LoginPayload, useLogin} from '../requests/use-login';\nimport {FormCheckbox} from '../../ui/forms/toggle/checkbox';\nimport {LinkStyle} from '../../ui/buttons/external-link';\nimport {SocialAuthSection} from './social-auth-section';\nimport {AuthLayout} from './auth-layout/auth-layout';\nimport {Trans} from '../../i18n/trans';\nimport {StaticPageTitle} from '../../seo/static-page-title';\nimport {useContext} from 'react';\nimport {\n SiteConfigContext,\n SiteConfigContextValue,\n} from '../../core/settings/site-config-context';\nimport {useSettings} from '../../core/settings/use-settings';\n\ninterface Props {\n onTwoFactorChallenge: () => void;\n}\nexport function LoginPage({onTwoFactorChallenge}: Props) {\n const [searchParams] = useSearchParams();\n const {pathname} = useLocation();\n\n const isWorkspaceLogin = pathname.includes('workspace');\n const searchParamsEmail = searchParams.get('email') || undefined;\n\n const {branding, registration, site, social} = useSettings();\n const siteConfig = useContext(SiteConfigContext);\n\n const demoDefaults =\n site.demo && !searchParamsEmail ? getDemoFormDefaults(siteConfig) : {};\n const form = useForm({\n defaultValues: {remember: true, email: searchParamsEmail, ...demoDefaults},\n });\n const login = useLogin(form);\n\n const heading = isWorkspaceLogin ? (\n \n ) : (\n \n );\n\n const message = !registration.disable && (\n (\n \n {parts}\n \n ),\n }}\n message=\"Don't have an account? Sign up.\"\n />\n );\n\n const isInvalid = !!Object.keys(form.formState.errors).length;\n\n return (\n \n \n \n \n {\n login.mutate(payload, {\n onSuccess: response => {\n if (response.two_factor) {\n onTwoFactorChallenge();\n }\n },\n });\n }}\n >\n }\n disabled={!!searchParamsEmail}\n invalid={isInvalid}\n required\n />\n }\n invalid={isInvalid}\n labelSuffix={\n \n \n \n }\n required\n />\n \n \n \n \n \n \n \n \n ) : (\n \n )\n }\n />\n \n );\n}\n\nfunction getDemoFormDefaults(siteConfig: SiteConfigContextValue) {\n if (siteConfig.demo.loginPageDefaults === 'randomAccount') {\n // random number between 0 and 100, padded to 3 digits\n const number = Math.floor(Math.random() * 100) + 1;\n const paddedNumber = String(number).padStart(3, '0');\n return {\n email: `admin@demo${paddedNumber}.com`,\n password: 'admin',\n };\n } else {\n return {\n email: 'admin@admin.com',\n password: 'admin',\n };\n }\n}\n","import {useState} from 'react';\nimport {TwoFactorChallengePage} from '@common/auth/ui/two-factor/two-factor-challenge-page';\nimport {LoginPage} from '@common/auth/ui/login-page';\n\nexport function LoginPageWrapper() {\n const [isTwoFactor, setIsTwoFactor] = useState(false);\n if (isTwoFactor) {\n return ;\n } else {\n return setIsTwoFactor(true)} />;\n }\n}\n","import {ReactElement} from 'react';\nimport {GuestRoute} from '../auth/guards/guest-route';\nimport {RegisterPage} from '../auth/ui/register-page';\nimport {useSettings} from '../core/settings/use-settings';\nimport {CustomPageLayout} from '@common/custom-page/custom-page-layout';\nimport {LoginPageWrapper} from '@common/auth/ui/login-page-wrapper';\n\ninterface DynamicHomepageProps {\n homepageResolver?: (type?: string) => ReactElement;\n}\nexport function DynamicHomepage({homepageResolver}: DynamicHomepageProps) {\n const {homepage} = useSettings();\n\n if (homepage?.type === 'loginPage') {\n return (\n \n \n \n );\n }\n\n if (homepage?.type === 'registerPage') {\n return (\n \n \n \n );\n }\n\n if (homepage?.type === 'customPage') {\n return ;\n }\n\n return homepageResolver?.(homepage?.type) || null;\n}\n","import {useAuth} from '../../auth/use-auth';\nimport {memo, useEffect, useId, useMemo, useRef} from 'react';\nimport lazyLoader from '../../utils/http/lazy-loader';\nimport clsx from 'clsx';\nimport {useSettings} from '../../core/settings/use-settings';\nimport dot from 'dot-object';\nimport {Settings} from '@common/core/settings/settings';\nimport {getScrollParent} from '@react-aria/utils';\n\ninterface AdHostProps {\n slot: keyof Omit, 'disable'>;\n className?: string;\n}\nexport function AdHost({slot, className}: AdHostProps) {\n const settings = useSettings();\n const {isSubscribed} = useAuth();\n const adCode = useMemo(() => {\n return dot.pick(`ads.${slot}`, settings);\n }, [slot, settings]);\n\n if (settings.ads?.disable || isSubscribed || !adCode) return null;\n\n return ;\n}\n\ninterface InvariantAdProps {\n slot: string;\n adCode: string;\n className?: string;\n}\nconst InvariantAd = memo(\n ({slot, adCode, className}: InvariantAdProps) => {\n const ref = useRef(null);\n\n const id = useId();\n\n useEffect(() => {\n if (ref.current) {\n loadAdScripts(adCode, ref.current).then(() => {\n executeAdJavascript(adCode, id);\n });\n }\n return () => {\n // @ts-ignore\n delete window['google_ad_modifications'];\n };\n }, [adCode, id]);\n\n // remove height modifications added by adsense\n useEffect(() => {\n if (ref.current) {\n const scrollParent = getScrollParent(ref.current) as HTMLElement;\n if (scrollParent) {\n const observer = new MutationObserver(function () {\n scrollParent.style.height = '';\n scrollParent.style.minHeight = '';\n });\n observer.observe(scrollParent, {\n attributes: true,\n attributeFilter: ['style'],\n });\n return () => observer.disconnect();\n }\n }\n }, []);\n\n return (\n \n );\n },\n () => {\n // never re-render\n return false;\n },\n);\n\nfunction getAdHtml(adCode: string) {\n // strip out all script tags from ad code and leave only html\n return adCode\n ?.replace(/)<[^<]*)*<\\/script>/gi, '')\n .trim();\n}\n\n// Load any external scripts needed by ad.\nfunction loadAdScripts(adCode: string, parentEl: HTMLDivElement): Promise {\n const promises = [];\n\n // load ad code script\n const pattern = /]*>([\\s\\S]*?)<\\/script>/g;\n let content;\n\n while ((content = pattern.exec(adCode))) {\n if (content[1]) {\n const r = `var d = document.createElement('div'); d.innerHTML = $1; document.getElementById('${id}').appendChild(d.firstChild);`;\n const toEval = content[1].replace(/document.write\\((.+?)\\);/, r);\n eval(toEval);\n }\n }\n}\n","import clsx from 'clsx';\nimport {LandingPageContent} from './landing-page-content';\nimport {Navbar} from '@common/ui/navigation/navbar/navbar';\nimport {Button, ButtonProps} from '@common/ui/buttons/button';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {KeyboardArrowDownIcon} from '@common/icons/material/KeyboardArrowDown';\nimport {MixedImage} from '@common/ui/images/mixed-image';\nimport {Footer} from '@common/ui/footer/footer';\nimport {Trans} from '@common/i18n/trans';\nimport {AdHost} from '@common/admin/ads/ad-host';\nimport {Link} from 'react-router-dom';\nimport {createSvgIconFromTree} from '@common/icons/create-svg-icon';\nimport {MenuItemConfig} from '@common/core/settings/settings';\nimport {Fragment} from 'react';\nimport {DefaultMetaTags} from '@common/seo/default-meta-tags';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {useSettings} from '@common/core/settings/use-settings';\n\ninterface ContentProps {\n content: LandingPageContent;\n}\nexport function LandingPage() {\n const settings = useSettings();\n const homepage = settings.homepage as {appearance: LandingPageContent};\n\n return (\n \n \n
\n \n \n \n
\n \n \n
\n
\n \n );\n}\n\nfunction HeroHeader({\n content: {\n headerTitle,\n headerSubtitle,\n headerImage,\n headerImageOpacity,\n actions,\n headerOverlayColor1,\n headerOverlayColor2,\n },\n}: ContentProps) {\n const {trans} = useTrans();\n\n let overlayBackground = undefined;\n\n if (headerOverlayColor1 && headerOverlayColor2) {\n overlayBackground = `linear-gradient(45deg, ${headerOverlayColor1} 0%, ${headerOverlayColor2} 100%)`;\n } else if (headerOverlayColor1) {\n overlayBackground = headerOverlayColor1;\n } else if (headerOverlayColor2) {\n overlayBackground = headerOverlayColor2;\n }\n\n return (\n \n \n
\n \n
\n {headerTitle && (\n \n \n \n )}\n {headerSubtitle && (\n \n \n
\n )}\n
\n \n \n
\n
\n
\n \n \n \n \n );\n}\n\ninterface CtaButtonProps extends ButtonProps {\n item?: MenuItemConfig;\n}\nfunction CtaButton({item, ...buttonProps}: CtaButtonProps) {\n if (!item?.label) return null;\n const Icon = item.icon ? createSvgIconFromTree(item.icon) : undefined;\n return (\n : undefined}\n {...buttonProps}\n >\n \n \n );\n}\n\nfunction PrimaryFeatures({content}: ContentProps) {\n return (\n \n {content?.primaryFeatures?.map((feature, index) => (\n \n \n \n \n \n \n \n \n \n ))}\n \n );\n}\n\nfunction SecondaryFeatures({content}: ContentProps) {\n return (\n
\n {content?.secondaryFeatures?.map((feature, index) => {\n const isEven = index % 2 === 0;\n return (\n \n \n
\n \n \n \n \n \n \n
\n \n \n
\n
\n
\n );\n })}\n \n );\n}\n\nfunction BottomCta({content}: ContentProps) {\n return (\n \n \n \n \n {content.footerSubtitle && (\n \n \n

\n )}\n \n \n );\n}\n","import {ReactElement} from 'react';\nimport {Navigate, Outlet} from 'react-router-dom';\nimport {useAuth} from '../use-auth';\nimport {NotFoundPage} from '@common/ui/not-found-page/not-found-page';\n\ninterface Props {\n children?: ReactElement;\n permission?: string;\n requireLogin?: boolean;\n}\nexport function AuthRoute({children, permission, requireLogin = true}: Props) {\n const {isLoggedIn, hasPermission} = useAuth();\n if (\n (requireLogin && !isLoggedIn) ||\n (permission && !hasPermission(permission))\n ) {\n if (isLoggedIn) {\n return ;\n }\n return ;\n }\n return children || ;\n}\n","import {ReactNode} from 'react';\n\ninterface Props {\n id: string;\n title: ReactNode;\n titleSuffix?: ReactNode;\n children: ReactNode;\n actions?: ReactNode;\n}\nexport function AccountSettingsPanel({\n id,\n title,\n titleSuffix,\n children,\n actions,\n}: Props) {\n return (\n \n
\n
{title}
\n {titleSuffix &&
{titleSuffix}
}\n
\n
{children}
\n {actions && (\n
{actions}
\n )}\n \n );\n}\n","import React, {forwardRef, ReactElement, ReactNode, useState} from 'react';\nimport {FocusScope, useFocusManager} from '@react-aria/focus';\nimport {ListItemBase, ListItemBaseProps} from './list-item-base';\nimport clsx from 'clsx';\n\ntype Child = ReactElement | ReactElement[];\n\ninterface Props {\n className?: string;\n padding?: string;\n children: ReactNode;\n dataTestId?: string;\n}\n\nexport function List({children, className, padding, dataTestId}: Props) {\n return (\n \n \n {children}\n \n \n );\n}\n\ninterface ListItemProps extends ListItemBaseProps {\n children: ReactNode;\n onSelected?: () => void;\n borderRadius?: string;\n}\nexport const ListItem = forwardRef(\n (\n {\n children,\n onSelected,\n borderRadius = 'rounded',\n className,\n ...listItemProps\n },\n ref,\n ) => {\n const focusManager = useFocusManager();\n const isSelectable = !!onSelected;\n const [isActive, setIsActive] = useState(false);\n\n const onKeyDown = (e: React.KeyboardEvent) => {\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n focusManager?.focusNext();\n break;\n case 'ArrowUp':\n e.preventDefault();\n focusManager?.focusPrevious();\n break;\n case 'Home':\n e.preventDefault();\n focusManager?.focusFirst();\n break;\n case 'End':\n e.preventDefault();\n focusManager?.focusLast();\n break;\n case 'Enter':\n case 'Space':\n e.preventDefault();\n onSelected?.();\n break;\n }\n };\n\n return (\n
  • \n {\n setIsActive((e.target as HTMLElement).matches(':focus-visible'));\n }}\n onBlur={() => {\n setIsActive(false);\n }}\n onClick={() => {\n onSelected?.();\n }}\n ref={ref}\n role={isSelectable ? 'button' : undefined}\n onKeyDown={isSelectable ? onKeyDown : undefined}\n tabIndex={isSelectable && !listItemProps.isDisabled ? 0 : undefined}\n >\n {children}\n \n
  • \n );\n },\n);\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const LoginIcon = createSvgIcon(\n \n, 'LoginOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const LockIcon = createSvgIcon(\n \n, 'LockOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const PhonelinkLockIcon = createSvgIcon(\n \n, 'PhonelinkLockOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const ApiIcon = createSvgIcon(\n \n, 'ApiOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const DangerousIcon = createSvgIcon(\n \n, 'DangerousOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const DevicesIcon = createSvgIcon(\n \n, 'DevicesOutlined');\n","import {List, ListItem} from '@common/ui/list/list';\nimport {PersonIcon} from '@common/icons/material/Person';\nimport {Trans} from '@common/i18n/trans';\nimport {LoginIcon} from '@common/icons/material/Login';\nimport {LockIcon} from '@common/icons/material/Lock';\nimport {PhonelinkLockIcon} from '@common/icons/material/PhonelinkLock';\nimport {LanguageIcon} from '@common/icons/material/Language';\nimport {ApiIcon} from '@common/icons/material/Api';\nimport {DangerousIcon} from '@common/icons/material/Dangerous';\nimport {ReactNode, useContext} from 'react';\nimport {DevicesIcon} from '@common/icons/material/Devices';\nimport {useAuth} from '@common/auth/use-auth';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\n\nexport enum AccountSettingsId {\n AccountDetails = 'account-details',\n SocialLogin = 'social-login',\n Password = 'password',\n TwoFactor = 'two-factor',\n LocationAndLanguage = 'location-and-language',\n Developers = 'developers',\n DeleteAccount = 'delete-account',\n Sessions = 'sessions',\n}\n\nexport function AccountSettingsSidenav() {\n const p = AccountSettingsId;\n\n const {hasPermission} = useAuth();\n const {api, social} = useSettings();\n const {auth} = useContext(SiteConfigContext);\n\n const socialEnabled =\n social?.envato || social?.google || social?.facebook || social?.twitter;\n\n return (\n \n );\n}\n\ninterface ItemProps {\n children: ReactNode;\n icon: ReactNode;\n isLast?: boolean;\n panel: AccountSettingsId;\n}\nfunction Item({children, icon, isLast, panel}: ItemProps) {\n return (\n {\n const panelEl = document.querySelector(`#${panel}`);\n if (panelEl) {\n panelEl.scrollIntoView({\n behavior: 'smooth',\n block: 'start',\n });\n }\n }}\n >\n {children}\n \n );\n}\n","import clsx from 'clsx';\nimport {cloneElement, ReactElement} from 'react';\nimport {SocialService, useSocialLogin} from '../../requests/use-social-login';\nimport {toast} from '@common/ui/toast/toast';\nimport {Button} from '@common/ui/buttons/button';\nimport {EnvatoIcon} from '@common/icons/social/envato';\nimport {GoogleIcon} from '@common/icons/social/google';\nimport {FacebookIcon} from '@common/icons/social/facebook';\nimport {TwitterIcon} from '@common/icons/social/twitter';\nimport {User} from '../../user';\nimport {AccountSettingsPanel} from './account-settings-panel';\nimport {Trans} from '@common/i18n/trans';\nimport {message} from '@common/i18n/message';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {queryClient} from '@common/http/query-client';\nimport {AccountSettingsId} from '@common/auth/ui/account-settings/account-settings-sidenav';\n\ninterface Props {\n user: User;\n}\nexport function SocialLoginPanel({user}: Props) {\n return (\n }\n >\n \n }\n service=\"envato\"\n user={user}\n />\n }\n service=\"google\"\n user={user}\n />\n }\n service=\"facebook\"\n user={user}\n />\n }\n service=\"twitter\"\n user={user}\n />\n
    \n \n
    \n \n );\n}\n\ninterface SocialLoginPanelRowProps {\n service: SocialService;\n user: User;\n className?: string;\n icon: ReactElement;\n}\n\nfunction SocialLoginPanelRow({\n service,\n user,\n className,\n icon,\n}: SocialLoginPanelRowProps) {\n const {social} = useSettings();\n const {connectSocial, disconnectSocial} = useSocialLogin();\n const username = user?.social_profiles?.find(s => s.service_name === service)\n ?.username;\n\n if (!social?.[service]?.enable) {\n return null;\n }\n\n return (\n \n {cloneElement(icon, {\n size: 'xl',\n className: clsx(icon.props.className, 'border p-8 rounded'),\n })}\n
    \n
    \n \n
    \n
    \n {username || }\n
    \n
    \n {\n if (username) {\n disconnectSocial.mutate(\n {service},\n {\n onSuccess: () => {\n queryClient.invalidateQueries({queryKey: ['users']});\n toast(\n message('Disabled :service account', {values: {service}}),\n );\n },\n },\n );\n } else {\n const e = await connectSocial(service);\n if (e?.status === 'SUCCESS') {\n queryClient.invalidateQueries({queryKey: ['users']});\n toast(message('Enabled :service account', {values: {service}}));\n }\n }\n }}\n >\n {username ? : }\n \n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {toast} from '@common/ui/toast/toast';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {User} from '@common/auth/user';\nimport {message} from '@common/i18n/message';\nimport {apiClient} from '@common/http/query-client';\n\ninterface Response extends BackendResponse {}\n\ninterface Payload {\n first_name?: string;\n last_name?: string;\n}\n\nexport function useUpdateAccountDetails(form: UseFormReturn>) {\n return useMutation({\n mutationFn: (props: Payload) => updateAccountDetails(props),\n onSuccess: () => {\n toast(message('Updated account details'));\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction updateAccountDetails(payload: Payload): Promise {\n return apiClient.put('users/me', payload).then(r => r.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {toast} from '@common/ui/toast/toast';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {UploadedFile} from '@common/uploads/uploaded-file';\nimport {User} from '@common/auth/user';\nimport {message} from '@common/i18n/message';\nimport {apiClient} from '@common/http/query-client';\nimport {getAxiosErrorMessage} from '@common/utils/http/get-axios-error-message';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {\n user: User;\n}\n\ninterface Payload {\n file?: UploadedFile;\n url?: string;\n}\n\ninterface UserProps {\n user: User;\n}\n\nfunction UploadAvatar({file, url}: Payload, user: User): Promise {\n const payload = new FormData();\n if (file) {\n payload.set('file', file.native);\n } else {\n payload.set('url', url!);\n }\n return apiClient\n .post(`users/${user.id}/avatar`, payload, {\n headers: {\n 'Content-Type': 'multipart/form-data',\n },\n })\n .then(r => r.data);\n}\n\nexport function useUploadAvatar({user}: UserProps) {\n return useMutation({\n mutationFn: (payload: Payload) => UploadAvatar(payload, user),\n onSuccess: () => {\n toast(message('Uploaded avatar'));\n },\n onError: err => {\n const message = getAxiosErrorMessage(err, 'file');\n if (message) {\n toast.danger(message);\n } else {\n showHttpErrorToast(err);\n }\n },\n });\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {toast} from '../../../../ui/toast/toast';\nimport {BackendResponse} from '../../../../http/backend-response/backend-response';\nimport {User} from '../../../user';\nimport {message} from '../../../../i18n/message';\nimport {apiClient} from '../../../../http/query-client';\nimport {showHttpErrorToast} from '../../../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\ninterface UserProps {\n user: User;\n}\n\nfunction removeAvatar(user: User): Promise {\n return apiClient.delete(`users/${user.id}/avatar`).then(r => r.data);\n}\n\nexport function useRemoveAvatar({user}: UserProps) {\n return useMutation({\n mutationFn: () => removeAvatar(user),\n onSuccess: () => {\n toast(message('Removed avatar'));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n","import {UploadStrategy, UploadStrategyConfig} from './upload-strategy';\nimport {UploadedFile} from '../../uploaded-file';\nimport axios, {AxiosInstance, AxiosProgressEvent} from 'axios';\nimport {FileEntry} from '../../file-entry';\nimport {\n getFromLocalStorage,\n removeFromLocalStorage,\n setInLocalStorage,\n} from '@common/utils/hooks/local-storage';\nimport {apiClient} from '@common/http/query-client';\nimport {getAxiosErrorMessage} from '@common/utils/http/get-axios-error-message';\nimport axiosRetry from 'axios-retry';\n\nconst oneMB = 1024 * 1024;\n// chunk size that will be uploaded to s3 per request\nconst desiredChunkSize = 20 * oneMB;\n// how many urls should be pre-signed per call to backend\nconst batchSize = 10;\n// number of concurrent requests to s3 api\nconst concurrency = 5;\n\ninterface ChunkState {\n blob: Blob | File;\n done: boolean;\n etag?: string;\n partNumber: number;\n bytesUploaded: number;\n}\n\ninterface SignedUrl {\n url: string;\n partNumber: number;\n}\n\ninterface StoredUrl {\n createdAt: string;\n uploadId: string;\n fileKey: string;\n}\n\ninterface UploadedPart {\n PartNumber: number;\n ETag: string;\n Size: string;\n LastModified: string;\n}\n\nexport class S3MultipartUpload implements UploadStrategy {\n private abortController: AbortController;\n private chunks: ChunkState[] = [];\n private uploadId?: string;\n private fileKey?: string;\n private readonly chunkAxios: AxiosInstance;\n private abortedByUser = false;\n private uploadedParts?: UploadedPart[];\n\n get storageKey(): string {\n return `s3-multipart::${this.file.fingerprint}`;\n }\n\n constructor(\n private file: UploadedFile,\n private config: UploadStrategyConfig\n ) {\n this.abortController = new AbortController();\n this.chunkAxios = axios.create();\n axiosRetry(this.chunkAxios, {retries: 3});\n }\n\n async start() {\n const storedUrl = getFromLocalStorage(this.storageKey);\n if (storedUrl) {\n await this.getUploadedParts(storedUrl);\n }\n\n if (!this.uploadedParts?.length) {\n await this.createMultipartUpload();\n if (!this.uploadId) return;\n }\n\n this.prepareChunks();\n\n const result = await this.uploadParts();\n if (result === 'done') {\n const isCompleted = await this.completeMultipartUpload();\n if (!isCompleted) return;\n\n // catch any errors so below \"onError\" handler gets executed\n try {\n const response = await this.createFileEntry();\n if (response?.fileEntry) {\n this.config.onSuccess?.(response?.fileEntry, this.file);\n removeFromLocalStorage(this.storageKey);\n return;\n }\n } catch {}\n }\n\n // upload failed\n if (!this.abortController.signal.aborted) {\n this.abortController.abort();\n }\n if (!this.abortedByUser) {\n this.config.onError?.(null, this.file);\n }\n }\n\n async abort() {\n this.abortedByUser = true;\n this.abortController.abort();\n await this.abortUploadOnS3();\n }\n\n private async uploadParts(): Promise {\n const pendingChunks = this.chunks.filter(c => !c.done);\n if (!pendingChunks.length) {\n return Promise.resolve('done');\n }\n\n const signedUrls = await this.batchSignUrls(\n pendingChunks.slice(0, batchSize)\n );\n if (!signedUrls) return;\n\n while (signedUrls.length) {\n const batch = signedUrls.splice(0, concurrency);\n const pendingUploads = batch.map(item => {\n return this.uploadPartToS3(item);\n });\n const result = await Promise.all(pendingUploads);\n // if not all uploads in batch completed, bail\n if (!result.every(r => r)) return;\n }\n\n return await this.uploadParts();\n }\n\n private async batchSignUrls(\n batch: ChunkState[]\n ): Promise {\n const response = await this.chunkAxios\n .post(\n 'api/v1/s3/multipart/batch-sign-part-urls',\n {\n partNumbers: batch.map(i => i.partNumber),\n uploadId: this.uploadId,\n key: this.fileKey,\n },\n {signal: this.abortController.signal}\n )\n .then(r => r.data as {urls: SignedUrl[]})\n .catch(err => {\n if (!this.abortController.signal.aborted) {\n this.abortController.abort();\n }\n });\n\n return response?.urls;\n }\n\n private async uploadPartToS3({\n url,\n partNumber,\n }: SignedUrl): Promise {\n const chunk = this.chunks.find(c => c.partNumber === partNumber);\n if (!chunk) return;\n return this.chunkAxios\n .put(url, chunk.blob, {\n withCredentials: false,\n signal: this.abortController.signal,\n onUploadProgress: (e: AxiosProgressEvent) => {\n if (!e.event.lengthComputable) return;\n\n chunk.bytesUploaded = e.loaded;\n const totalUploaded = this.chunks.reduce(\n (n, c) => n + c.bytesUploaded,\n 0\n );\n\n this.config.onProgress?.({\n bytesUploaded: totalUploaded,\n bytesTotal: this.file.size,\n });\n },\n })\n .then(r => {\n const etag = r.headers.etag;\n if (etag) {\n chunk.done = true;\n chunk.etag = etag;\n return true;\n }\n })\n .catch(err => {\n if (!this.abortController.signal.aborted && err !== undefined) {\n this.abortController.abort();\n }\n });\n }\n\n private async createMultipartUpload(): Promise {\n const response = await apiClient\n .post('s3/multipart/create', {\n filename: this.file.name,\n mime: this.file.mime,\n size: this.file.size,\n extension: this.file.extension,\n ...this.config.metadata,\n })\n .then(r => r.data as {uploadId: string; key: string})\n .catch(err => {\n if (err.code !== 'ERR_CANCELED') {\n this.config.onError?.(getAxiosErrorMessage(err), this.file);\n }\n });\n\n if (response) {\n this.uploadId = response.uploadId;\n this.fileKey = response.key;\n setInLocalStorage(this.storageKey, {\n createdAt: new Date().toISOString(),\n fileKey: this.fileKey,\n uploadId: this.uploadId,\n } as StoredUrl);\n }\n }\n\n private async getUploadedParts({fileKey, uploadId}: StoredUrl) {\n const response = await apiClient\n .post('s3/multipart/get-uploaded-parts', {\n key: fileKey,\n uploadId,\n })\n .then(r => r.data as {parts: UploadedPart[]})\n .catch(() => {\n removeFromLocalStorage(this.storageKey);\n return null;\n });\n if (response?.parts?.length) {\n this.uploadedParts = response.parts;\n this.uploadId = uploadId;\n this.fileKey = fileKey;\n }\n }\n\n private async completeMultipartUpload(): Promise<{location: string} | null> {\n return apiClient\n .post('s3/multipart/complete', {\n key: this.fileKey,\n uploadId: this.uploadId,\n parts: this.chunks.map(c => {\n return {\n ETag: c.etag,\n PartNumber: c.partNumber,\n };\n }),\n })\n .then(r => r.data)\n .catch(() => {\n this.config.onError?.(null, this.file);\n this.abortUploadOnS3();\n })\n .finally(() => {\n removeFromLocalStorage(this.storageKey);\n });\n }\n\n private async createFileEntry(): Promise<{fileEntry: FileEntry}> {\n return await apiClient\n .post('s3/entries', {\n ...this.config.metadata,\n clientMime: this.file.mime,\n clientName: this.file.name,\n filename: this.fileKey!.split('/').pop(),\n size: this.file.size,\n clientExtension: this.file.extension,\n })\n .then(r => r.data)\n .catch();\n }\n\n private prepareChunks() {\n this.chunks = [];\n // at least 5MB per request, at most 10k requests\n const minChunkSize = Math.max(5 * oneMB, Math.ceil(this.file.size / 10000));\n const chunkSize = Math.max(desiredChunkSize, minChunkSize);\n\n // Upload zero-sized files in one zero-sized chunk\n if (this.file.size === 0) {\n this.chunks.push({\n blob: this.file.native,\n done: false,\n partNumber: 1,\n bytesUploaded: 0,\n });\n } else {\n let partNumber = 1;\n for (let i = 0; i < this.file.size; i += chunkSize) {\n const end = Math.min(this.file.size, i + chunkSize);\n // check if this part was already uploaded previously\n const previouslyUploaded = this.uploadedParts?.find(\n p => p.PartNumber === partNumber\n );\n this.chunks.push({\n blob: this.file.native.slice(i, end),\n done: !!previouslyUploaded,\n partNumber,\n etag: previouslyUploaded ? previouslyUploaded.ETag : undefined,\n bytesUploaded: previouslyUploaded?.Size\n ? parseInt(previouslyUploaded?.Size)\n : 0,\n });\n partNumber++;\n }\n }\n }\n\n private abortUploadOnS3() {\n return apiClient.post('s3/multipart/abort', {\n key: this.fileKey,\n uploadId: this.uploadId,\n });\n }\n\n static async create(\n file: UploadedFile,\n config: UploadStrategyConfig\n ): Promise {\n return new S3MultipartUpload(file, config);\n }\n}\n","import {Upload} from 'tus-js-client';\nimport {UploadedFile} from '../../uploaded-file';\nimport {UploadStrategy, UploadStrategyConfig} from './upload-strategy';\nimport {FileEntry} from '../../file-entry';\nimport {getAxiosErrorMessage} from '@common/utils/http/get-axios-error-message';\nimport {apiClient} from '@common/http/query-client';\nimport {getCookie} from 'react-use-cookie';\n\nexport class TusUpload implements UploadStrategy {\n constructor(private upload: Upload) {}\n\n start() {\n this.upload.start();\n }\n\n abort() {\n return this.upload.abort(true);\n }\n\n static async create(\n file: UploadedFile,\n {\n onProgress,\n onSuccess,\n onError,\n metadata,\n chunkSize,\n baseUrl,\n }: UploadStrategyConfig\n ): Promise {\n const tusFingerprint = ['tus', file.fingerprint, 'drive'].join('-');\n const upload = new Upload(file.native, {\n fingerprint: () => Promise.resolve(tusFingerprint),\n removeFingerprintOnSuccess: true,\n endpoint: `${baseUrl}/api/v1/tus/upload`,\n chunkSize,\n retryDelays: [0, 3000, 5000, 10000, 20000],\n overridePatchMethod: true,\n metadata: {\n name: window.btoa(file.id),\n clientName: file.name,\n clientExtension: file.extension,\n clientMime: file.mime || '',\n clientSize: `${file.size}`,\n ...(metadata as Record),\n },\n headers: {\n 'X-XSRF-TOKEN': getCookie('XSRF-TOKEN'),\n },\n onError: err => {\n if ('originalResponse' in err && err.originalResponse) {\n try {\n const message = JSON.parse(err.originalResponse.getBody())?.message;\n onError?.(message, file);\n } catch (e) {\n onError?.(null, file);\n }\n } else {\n onError?.(null, file);\n }\n },\n onProgress(bytesUploaded, bytesTotal) {\n onProgress?.({bytesUploaded, bytesTotal});\n },\n onSuccess: async () => {\n const uploadKey = upload.url?.split('/').pop();\n try {\n if (uploadKey) {\n const response = await createFileEntry(uploadKey);\n onSuccess?.(response.fileEntry, file);\n }\n } catch (err) {\n localStorage.removeItem(tusFingerprint);\n onError?.(getAxiosErrorMessage(err), file);\n }\n },\n });\n\n const previousUploads = await upload.findPreviousUploads();\n if (previousUploads.length) {\n upload.resumeFromPreviousUpload(previousUploads[0]);\n }\n\n return new TusUpload(upload);\n }\n}\n\nfunction createFileEntry(uploadKey: string): Promise<{fileEntry: FileEntry}> {\n return apiClient.post('tus/entries', {uploadKey}).then(r => r.data);\n}\n","export interface BackendMetadata {\n disk?: Disk;\n diskPrefix?: string;\n relativePath?: string | null;\n [key: string]: number | string | null | undefined;\n}\n\nexport enum Disk {\n public = 'public',\n uploads = 'uploads',\n}\n","import {UploadStrategy, UploadStrategyConfig} from './upload-strategy';\nimport {UploadedFile} from '../../uploaded-file';\nimport axios, {AxiosProgressEvent} from 'axios';\nimport {FileEntry} from '../../file-entry';\nimport {getAxiosErrorMessage} from '@common/utils/http/get-axios-error-message';\nimport {apiClient} from '@common/http/query-client';\n\ninterface PresignedRequest {\n url: string;\n key: string;\n acl: string;\n}\n\nexport class S3Upload implements UploadStrategy {\n private abortController: AbortController;\n private presignedRequest?: PresignedRequest;\n\n constructor(\n private file: UploadedFile,\n private config: UploadStrategyConfig\n ) {\n this.abortController = new AbortController();\n }\n\n async start() {\n this.presignedRequest = await this.presignPostUrl();\n if (!this.presignedRequest) return;\n\n const result = await this.uploadFileToS3();\n if (result !== 'uploaded') return;\n\n const response = await this.createFileEntry();\n if (response?.fileEntry) {\n this.config.onSuccess?.(response.fileEntry, this.file);\n } else if (!this.abortController.signal) {\n this.config.onError?.(null, this.file);\n }\n }\n\n abort() {\n this.abortController.abort();\n return Promise.resolve();\n }\n\n private presignPostUrl(): Promise {\n return apiClient\n .post(\n 's3/simple/presign',\n {\n filename: this.file.name,\n mime: this.file.mime,\n disk: this.config.metadata?.disk,\n size: this.file.size,\n extension: this.file.extension,\n ...this.config.metadata,\n },\n {signal: this.abortController.signal}\n )\n .then(r => r.data)\n .catch(err => {\n if (err.code !== 'ERR_CANCELED') {\n this.config.onError?.(getAxiosErrorMessage(err), this.file);\n }\n });\n }\n\n private uploadFileToS3() {\n const {url, acl} = this.presignedRequest!;\n return axios\n .put(url, this.file.native, {\n signal: this.abortController.signal,\n withCredentials: false,\n headers: {\n 'Content-Type': this.file.mime,\n 'x-amz-acl': acl,\n },\n onUploadProgress: (e: AxiosProgressEvent) => {\n if (e.event.lengthComputable) {\n this.config.onProgress?.({\n bytesUploaded: e.loaded,\n bytesTotal: e.total || 0,\n });\n }\n },\n })\n .then(() => 'uploaded')\n .catch(err => {\n if (err.code !== 'ERR_CANCELED') {\n this.config.onError?.(getAxiosErrorMessage(err), this.file);\n }\n });\n }\n\n private async createFileEntry() {\n return await apiClient\n .post('s3/entries', {\n ...this.config.metadata,\n clientMime: this.file.mime,\n clientName: this.file.name,\n filename: this.presignedRequest!.key.split('/').pop(),\n size: this.file.size,\n clientExtension: this.file.extension,\n })\n .then(r => {\n return r.data as {fileEntry: FileEntry};\n })\n .catch(err => {\n if (err.code !== 'ERR_CANCELED') {\n this.config.onError?.(getAxiosErrorMessage(err), this.file);\n }\n });\n }\n\n static async create(\n file: UploadedFile,\n config: UploadStrategyConfig\n ): Promise {\n return new S3Upload(file, config);\n }\n}\n","import {UploadedFile} from '../../uploaded-file';\nimport {UploadStrategy, UploadStrategyConfig} from './upload-strategy';\nimport {apiClient} from '@common/http/query-client';\nimport {getAxiosErrorMessage} from '@common/utils/http/get-axios-error-message';\nimport {AxiosProgressEvent} from 'axios';\n\nexport class AxiosUpload implements UploadStrategy {\n private abortController: AbortController;\n constructor(\n private file: UploadedFile,\n private config: UploadStrategyConfig,\n ) {\n this.abortController = new AbortController();\n }\n\n async start() {\n const formData = new FormData();\n const {onSuccess, onError, onProgress, metadata} = this.config;\n\n formData.set('file', this.file.native);\n formData.set('workspaceId', `12`);\n if (metadata) {\n Object.entries(metadata).forEach(([key, value]) => {\n formData.set(key, `${value}`);\n });\n }\n\n const response = await apiClient\n .post('file-entries', formData, {\n onUploadProgress: (e: AxiosProgressEvent) => {\n if (e.event.lengthComputable) {\n onProgress?.({\n bytesUploaded: e.loaded,\n bytesTotal: e.total || 0,\n });\n }\n },\n signal: this.abortController.signal,\n headers: {\n 'Content-Type': 'multipart/form-data',\n },\n })\n .catch(err => {\n if (err.code !== 'ERR_CANCELED') {\n onError?.(getAxiosErrorMessage(err), this.file);\n }\n });\n\n // if upload was aborted, it will be handled and set\n // as \"aborted\" already, no need to set it as \"failed\"\n if (this.abortController.signal.aborted) {\n return;\n }\n\n if (response && response.data.fileEntry) {\n onSuccess?.(response.data.fileEntry, this.file);\n }\n }\n\n abort() {\n this.abortController.abort();\n return Promise.resolve();\n }\n\n static async create(\n file: UploadedFile,\n config: UploadStrategyConfig,\n ): Promise {\n return new AxiosUpload(file, config);\n }\n}\n","// Adapted from https://github.com/Flet/prettier-bytes/\n// Changing 1000 bytes to 1024, so we can keep uppercase KB vs kB\n// ISC License (c) Dan Flettre https://github.com/Flet/prettier-bytes/blob/master/LICENSE\nexport function prettyBytes(num?: number, fractionDigits = 1): string {\n if (num == null || Number.isNaN(num)) return '';\n const neg = num < 0;\n const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];\n\n if (neg) {\n num = -num;\n }\n\n if (num < 1) {\n return `${(neg ? '-' : '') + num} B`;\n }\n\n const exponent = Math.min(\n Math.floor(Math.log(num) / Math.log(1024)),\n units.length - 1\n );\n num = Number(num / Math.pow(1024, exponent));\n const unit = units[exponent];\n\n if (num >= 10 || num % 1 === 0) {\n // Do not show decimals when the number is two-digit, or if the number has no\n // decimal component.\n return `${(neg ? '-' : '') + num.toFixed(0)} ${unit}`;\n }\n return `${(neg ? '-' : '') + num.toFixed(fractionDigits)} ${unit}`;\n}\n","import {UploadedFile} from '../uploaded-file';\nimport {message} from '../../i18n/message';\nimport {prettyBytes} from '../utils/pretty-bytes';\nimport {MessageDescriptor} from '../../i18n/message-descriptor';\nimport match from 'mime-match';\n\nexport interface Restrictions {\n maxFileSize?: number;\n allowedFileTypes?: string[];\n blockedFileTypes?: string[];\n}\n\nexport function validateUpload(\n file: UploadedFile,\n restrictions?: Restrictions\n): MessageDescriptor | void {\n if (!restrictions) return;\n\n const {maxFileSize, allowedFileTypes, blockedFileTypes} = restrictions;\n\n if (maxFileSize && file.size != null && file.size > maxFileSize) {\n return message('`:file` exceeds maximum allowed size of :size', {\n values: {file: file.name, size: prettyBytes(maxFileSize)},\n });\n }\n\n if (allowedFileTypes?.length) {\n if (!fileMatchesTypes(file, allowedFileTypes)) {\n return message('This file type is not allowed');\n }\n }\n\n if (blockedFileTypes?.length) {\n if (fileMatchesTypes(file, blockedFileTypes)) {\n return message('This file type is not allowed');\n }\n }\n}\n\nfunction fileMatchesTypes(file: UploadedFile, types: string[]): boolean {\n return (\n types\n // support multiple file types in a string (video/mp4,audio/mp3,image/png)\n .map(type => type.split(','))\n .flat()\n .some(type => {\n // check if this is a mime-type\n if (type.includes('/')) {\n if (!file.mime) return false;\n return match(file.mime.replace(/;.*?$/, ''), type);\n }\n\n // otherwise this is likely an extension\n const extension = type.replace('.', '').toLowerCase();\n if (extension && file.extension) {\n return file.extension.toLowerCase() === extension;\n }\n return false;\n })\n );\n}\n","export class ProgressTimeout {\n public aliveTimer: any;\n public isDone = false;\n public timeout = 30000;\n public timeoutHandler: (() => void) | null = null;\n\n progress() {\n // Some browsers fire another progress event when the upload is\n // cancelled, so we have to ignore progress after the timer was\n // told to stop.\n if (this.isDone || !this.timeoutHandler) return;\n\n if (this.timeout > 0) {\n clearTimeout(this.aliveTimer);\n this.aliveTimer = setTimeout(this.timeoutHandler, this.timeout);\n }\n }\n\n done() {\n if (!this.isDone) {\n clearTimeout(this.aliveTimer);\n this.aliveTimer = null;\n this.isDone = true;\n }\n }\n}\n","import {UploadStrategy, UploadStrategyConfig} from './strategy/upload-strategy';\nimport {UploadedFile} from '../uploaded-file';\nimport {Disk} from '../types/backend-metadata';\nimport {S3MultipartUpload} from './strategy/s3-multipart-upload';\nimport {S3Upload} from './strategy/s3-upload';\nimport {TusUpload} from './strategy/tus-upload';\nimport {AxiosUpload} from './strategy/axios-upload';\nimport {FileUpload, FileUploadState} from './file-upload-store';\nimport {validateUpload} from './validate-upload';\nimport {getBootstrapData} from '../../core/bootstrap-data/use-backend-bootstrap-data';\nimport {toast} from '../../ui/toast/toast';\nimport {ProgressTimeout} from './progress-timeout';\nimport {message} from '../../i18n/message';\n\nexport async function startUploading(\n upload: FileUpload,\n state: FileUploadState\n): Promise {\n const settings = getBootstrapData().settings;\n const options = upload.options;\n const file = upload.file;\n\n // validate file, if validation fails, error the upload and bail\n if (options?.restrictions) {\n const errorMessage = validateUpload(file, options.restrictions);\n if (errorMessage) {\n state.updateFileUpload(file.id, {\n errorMessage,\n status: 'failed',\n request: undefined,\n timer: undefined,\n });\n\n if (options.showToastOnRestrictionFail) {\n toast.danger(errorMessage);\n }\n\n state.runQueue();\n return null;\n }\n }\n\n // prepare config for file upload strategy\n const timer = new ProgressTimeout();\n const config: UploadStrategyConfig = {\n metadata: {\n ...options?.metadata,\n relativePath: file.relativePath,\n disk: options?.metadata?.disk || Disk.uploads,\n parentId: options?.metadata?.parentId || '',\n },\n chunkSize: settings.uploads.chunk_size,\n baseUrl: settings.base_url,\n onError: errorMessage => {\n state.updateFileUpload(file.id, {\n errorMessage,\n status: 'failed',\n });\n state.runQueue();\n timer.done();\n options?.onError?.(errorMessage, file);\n },\n onSuccess: entry => {\n state.updateFileUpload(file.id, {\n status: 'completed',\n entry,\n });\n state.runQueue();\n timer.done();\n options?.onSuccess?.(entry, file);\n },\n onProgress: ({bytesUploaded, bytesTotal}) => {\n const percentage = (bytesUploaded / bytesTotal) * 100;\n state.updateFileUpload(file.id, {\n percentage,\n bytesUploaded,\n });\n timer.progress();\n options?.onProgress?.({bytesUploaded, bytesTotal});\n },\n };\n\n // choose and create upload strategy, based on file size and settings\n const strategy = chooseUploadStrategy(file, config);\n const request = await strategy.create(file, config);\n\n // add handler for when upload times out (no progress for 30+ seconds)\n timer.timeoutHandler = () => {\n request.abort();\n state.updateFileUpload(file.id, {\n status: 'failed',\n errorMessage: message('Upload timed out'),\n });\n state.runQueue();\n };\n\n state.updateFileUpload(file.id, {\n status: 'inProgress',\n request,\n });\n request.start();\n\n return request;\n}\n\nconst OneMB = 1024 * 1024;\nconst FourMB = 4 * OneMB;\nconst HundredMB = 100 * OneMB;\n\nconst chooseUploadStrategy = (\n file: UploadedFile,\n config: UploadStrategyConfig\n) => {\n const settings = getBootstrapData().settings;\n const disk = config.metadata?.disk || Disk.uploads;\n const driver =\n disk === Disk.uploads\n ? settings.uploads.uploads_driver\n : settings.uploads.public_driver;\n\n if (driver?.endsWith('s3') && settings.uploads.s3_direct_upload) {\n return file.size >= HundredMB ? S3MultipartUpload : S3Upload;\n } else {\n // 4MB = Axios, otherwise Tus\n return file.size >= FourMB && !settings.uploads.disable_tus\n ? TusUpload\n : AxiosUpload;\n }\n};\n","export function extensionFromFilename(fullFileName: string): string {\n const re = /(?:\\.([^.]+))?$/;\n return re.exec(fullFileName)?.[1] || '';\n}\n","import {extensionFromFilename} from './extension-from-filename';\n\nexport function getFileMime(file: File): string {\n const extensionsToMime: Record = {\n md: 'text/markdown',\n markdown: 'text/markdown',\n mp4: 'video/mp4',\n mp3: 'audio/mp3',\n svg: 'image/svg+xml',\n jpg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n yaml: 'text/yaml',\n yml: 'text/yaml',\n };\n\n const fileExtension = file.name ? extensionFromFilename(file.name) : null;\n\n // check if mime type is set in the file object\n if (file.type) {\n return file.type;\n }\n\n // see if we can map extension to a mime type\n if (fileExtension && fileExtension in extensionsToMime) {\n return extensionsToMime[fileExtension];\n }\n\n return 'application/octet-stream';\n}\n","import {getFileMime} from './utils/get-file-mime';\nimport {extensionFromFilename} from './utils/extension-from-filename';\nimport {nanoid} from 'nanoid';\nimport {getActiveWorkspaceId} from '../workspace/active-workspace-id';\n\nexport class UploadedFile {\n id: string;\n fingerprint: string;\n name: string;\n relativePath = '';\n size: number;\n mime = '';\n extension = '';\n native: File;\n lastModified: number;\n\n private cachedData?: string;\n get data(): Promise {\n return new Promise(resolve => {\n if (this.cachedData) {\n resolve(this.cachedData);\n }\n const reader = new FileReader();\n\n reader.addEventListener('load', () => {\n this.cachedData = reader.result as string;\n resolve(this.cachedData);\n });\n\n if (this.extension === 'json') {\n reader.readAsText(this.native);\n } else {\n reader.readAsDataURL(this.native);\n }\n });\n }\n\n constructor(file: File, relativePath?: string | null) {\n this.id = nanoid();\n this.name = file.name;\n this.size = file.size;\n this.mime = getFileMime(file);\n this.lastModified = file.lastModified;\n this.extension = extensionFromFilename(file.name) || 'bin';\n this.native = file;\n relativePath = relativePath || file.webkitRelativePath || '';\n\n // remove leading slashes\n relativePath = relativePath.replace(/^\\/+/g, '');\n\n // only include relative path if file is actually in a folder and not just /file.txt\n if (relativePath && relativePath.split('/').length > 1) {\n this.relativePath = relativePath;\n }\n\n this.fingerprint = generateId({\n name: this.name,\n size: this.size,\n mime: this.mime,\n lastModified: this.lastModified,\n });\n }\n}\n\ninterface FileMeta {\n name?: string;\n mime?: string | null;\n size?: number | string;\n lastModified?: number;\n relativePath?: string;\n}\nfunction generateId({name, mime, size, relativePath, lastModified}: FileMeta) {\n let id = 'be';\n if (typeof name === 'string') {\n id += `-${encodeFilename(name.toLowerCase())}`;\n }\n\n if (mime) {\n id += `-${mime}`;\n }\n\n if (typeof relativePath === 'string') {\n id += `-${encodeFilename(relativePath.toLowerCase())}`;\n }\n\n if (size !== undefined) {\n id += `-${size}`;\n }\n if (lastModified !== undefined) {\n id += `-${lastModified}`;\n }\n\n id += `${getActiveWorkspaceId()}`;\n\n // add version number, so it can be incremented easily to allow uploading same file multiple times\n return `${id}-v1`;\n}\n\nfunction encodeCharacter(character: string) {\n return character.charCodeAt(0).toString(32);\n}\n\nfunction encodeFilename(name: string) {\n let suffix = '';\n return (\n name.replace(/[^A-Z0-9]/gi, character => {\n suffix += `-${encodeCharacter(character)}`;\n return '/';\n }) + suffix\n );\n}\n","import {UploadedFile} from '../uploaded-file';\nimport {UploadStrategyConfig} from './strategy/upload-strategy';\nimport {FileUpload} from './file-upload-store';\n\nexport function createUpload(\n file: UploadedFile | File,\n options?: UploadStrategyConfig\n): FileUpload {\n const uploadedFile =\n file instanceof UploadedFile ? file : new UploadedFile(file);\n return {\n file: uploadedFile,\n percentage: 0,\n bytesUploaded: 0,\n status: 'pending',\n options: options || {},\n };\n}\n","import {create} from 'zustand';\nimport {immer} from 'zustand/middleware/immer';\nimport {Draft, enableMapSet} from 'immer';\nimport {UploadedFile} from '../uploaded-file';\nimport {UploadStrategy, UploadStrategyConfig} from './strategy/upload-strategy';\nimport {MessageDescriptor} from '../../i18n/message-descriptor';\nimport {FileEntry} from '../file-entry';\nimport {S3MultipartUpload} from './strategy/s3-multipart-upload';\nimport {Settings} from '../../core/settings/settings';\nimport {TusUpload} from './strategy/tus-upload';\nimport {ProgressTimeout} from './progress-timeout';\nimport {startUploading} from './start-uploading';\nimport {createUpload} from './create-file-upload';\n\nenableMapSet();\n\nexport interface FileUpload {\n file: UploadedFile;\n percentage: number;\n bytesUploaded: number;\n status: 'pending' | 'inProgress' | 'aborted' | 'failed' | 'completed';\n errorMessage?: string | MessageDescriptor | null;\n entry?: FileEntry;\n request?: UploadStrategy;\n timer?: ProgressTimeout;\n options: UploadStrategyConfig;\n meta?: unknown;\n}\n\nexport interface FileUploadState {\n concurrency: number;\n fileUploads: Map;\n // uploads with pending and inProgress status\n activeUploadsCount: number;\n completedUploadsCount: number;\n uploadMultiple: (\n files: (File | UploadedFile)[] | FileList,\n options?: Omit<\n UploadStrategyConfig,\n // progress would be called for each upload simultaneously\n 'onProgress' | 'showToastOnRestrictionFail'\n >\n ) => string[];\n uploadSingle: (\n file: File | UploadedFile,\n options?: UploadStrategyConfig\n ) => string;\n clearInactive: () => void;\n abortUpload: (id: string) => void;\n updateFileUpload: (id: string, state: Partial) => void;\n getUpload: (id: string) => FileUpload | undefined;\n runQueue: () => void;\n}\n\ninterface StoreProps {\n settings: Settings;\n}\nexport const createFileUploadStore = ({settings}: StoreProps) =>\n create()(\n immer((set, get) => {\n return {\n concurrency: 3,\n fileUploads: new Map(),\n activeUploadsCount: 0,\n completedUploadsCount: 0,\n\n getUpload: uploadId => {\n return get().fileUploads.get(uploadId);\n },\n\n clearInactive: () => {\n set(state => {\n state.fileUploads.forEach((upload, key) => {\n if (upload.status !== 'inProgress') {\n state.fileUploads.delete(key);\n }\n });\n });\n get().runQueue();\n },\n\n abortUpload: id => {\n const upload = get().fileUploads.get(id);\n if (upload) {\n upload.request?.abort();\n get().updateFileUpload(id, {status: 'aborted', percentage: 0});\n get().runQueue();\n }\n },\n\n updateFileUpload: (id, newUploadState) => {\n set(state => {\n const fileUpload = state.fileUploads.get(id);\n if (fileUpload) {\n state.fileUploads.set(id, {\n ...fileUpload,\n ...newUploadState,\n });\n\n // only need to update inProgress count if status of the uploads in queue change\n if ('status' in newUploadState) {\n updateTotals(state);\n }\n }\n });\n },\n\n uploadSingle: (file, userOptions) => {\n const upload = createUpload(file, userOptions);\n const fileUploads = new Map(get().fileUploads);\n fileUploads.set(upload.file.id, upload);\n\n set(state => {\n updateTotals(state);\n state.fileUploads = fileUploads;\n });\n\n get().runQueue();\n\n return upload.file.id;\n },\n\n uploadMultiple: (files, options) => {\n // create file upload items from specified files\n const uploads = new Map(get().fileUploads);\n [...files].forEach(file => {\n const upload = createUpload(file, options);\n uploads.set(upload.file.id, upload);\n });\n\n // set state only once, there might be thousands of files, don't want to trigger a rerender for each one\n set(state => {\n updateTotals(state);\n state.fileUploads = uploads;\n });\n\n get().runQueue();\n return [...uploads.keys()];\n },\n\n runQueue: async () => {\n const uploads = [...get().fileUploads.values()];\n const activeUploads = uploads.filter(u => u.status === 'inProgress');\n\n let concurrency = get().concurrency;\n if (\n activeUploads.filter(\n activeUpload =>\n // only upload one file from folder at a time to avoid creating duplicate folders\n activeUpload.file.relativePath ||\n // only allow one s3 multipart upload at a time, it will already upload multiple parts in parallel\n activeUpload.request instanceof S3MultipartUpload ||\n // only allow one tus upload if file is larger than chunk size, tus will have parallel uploads already in that case\n (activeUpload.request instanceof TusUpload &&\n settings.uploads.chunk_size &&\n activeUpload.file.size > settings.uploads.chunk_size)\n ).length\n ) {\n concurrency = 1;\n }\n\n if (activeUploads.length < concurrency) {\n //const pendingUploads = uploads.filter(u => u.status === 'pending');\n //const next = pendingUploads.find(a => !!a.request);\n const next = uploads.find(u => u.status === 'pending');\n if (next) {\n await startUploading(next, get());\n }\n }\n },\n };\n })\n );\n\nconst updateTotals = (state: Draft) => {\n state.completedUploadsCount = [...state.fileUploads.values()].filter(\n u => u.status === 'completed'\n ).length;\n state.activeUploadsCount = [...state.fileUploads.values()].filter(\n u => u.status === 'inProgress' || u.status === 'pending'\n ).length;\n};\n","import {StoreApi, useStore} from 'zustand';\nimport {createContext, ReactNode, useContext, useState} from 'react';\nimport {createFileUploadStore, FileUploadState} from './file-upload-store';\nimport {useSettings} from '../../core/settings/use-settings';\n\nconst FileUploadContext = createContext>(null!);\n\ntype ExtractState = S extends {\n getState: () => infer T;\n}\n ? T\n : never;\n\ntype UseFileUploadStore = {\n (): ExtractState>;\n (\n selector: (state: ExtractState>) => U,\n equalityFn?: (a: U, b: U) => boolean\n ): U;\n};\n\n// @ts-ignore\nexport const useFileUploadStore: UseFileUploadStore = (\n selector,\n equalityFn\n) => {\n const store = useContext(FileUploadContext);\n return useStore(store, selector, equalityFn);\n};\n\ninterface FileUploadProviderProps {\n children: ReactNode;\n}\nexport function FileUploadProvider({children}: FileUploadProviderProps) {\n const settings = useSettings();\n\n //lazily create store object only once\n const [store] = useState(() => {\n return createFileUploadStore({settings});\n });\n\n return (\n }>\n {children}\n \n );\n}\n","import {UploadInputConfig} from '../types/upload-input-config';\n\nexport function createUploadInput(\n config: UploadInputConfig = {}\n): HTMLInputElement {\n const old = document.querySelector('#hidden-file-upload-input');\n if (old) old.remove();\n\n const input = document.createElement('input');\n input.type = 'file';\n input.multiple = config.multiple ?? false;\n input.classList.add('hidden');\n input.style.display = 'none';\n input.style.visibility = 'hidden';\n input.id = 'hidden-file-upload-input';\n\n input.accept = buildUploadInputAccept(config);\n\n if (config.directory) {\n input.webkitdirectory = true;\n }\n\n document.body.appendChild(input);\n\n return input;\n}\n\nexport interface UploadAccentProps {\n extensions?: string[];\n types?: string[];\n}\nexport function buildUploadInputAccept({\n extensions = [],\n types = [],\n}: UploadAccentProps): string {\n const accept = [];\n if (extensions?.length) {\n extensions = extensions.map(e => {\n return e.startsWith('.') ? e : `.${e}`;\n });\n accept.push(extensions.join(','));\n }\n\n if (types?.length) {\n accept.push(types.join(','));\n }\n\n return accept.join(',');\n}\n","import {UploadInputConfig} from '../types/upload-input-config';\nimport {UploadedFile} from '../uploaded-file';\nimport {createUploadInput} from './create-upload-input';\n\n/**\n * Open browser dialog for uploading files and\n * resolve promise with uploaded files.\n */\nexport function openUploadWindow(\n config: UploadInputConfig = {}\n): Promise {\n return new Promise(resolve => {\n const input = createUploadInput(config);\n\n input.onchange = e => {\n const fileList = (e.target as HTMLInputElement).files;\n if (!fileList) {\n return resolve([]);\n }\n\n const uploads = Array.from(fileList)\n .filter(f => f.name !== '.DS_Store')\n .map(file => new UploadedFile(file));\n resolve(uploads);\n input.remove();\n };\n\n document.body.appendChild(input);\n input.click();\n });\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {apiClient} from '../../http/query-client';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\ninterface Payload {\n entryIds?: number[];\n deleteForever?: boolean;\n paths?: string[];\n}\n\nfunction deleteFileEntries(payload: Payload): Promise {\n return apiClient.post('file-entries/delete', payload).then(r => r.data);\n}\n\nexport function useDeleteFileEntries() {\n return useMutation({\n mutationFn: (props: Payload) => deleteFileEntries(props),\n onError: err => showHttpErrorToast(err),\n });\n}\n","import {useCallback, useRef} from 'react';\nimport {useFileUploadStore} from './file-upload-provider';\nimport {UploadedFile} from '../uploaded-file';\nimport {UploadStrategyConfig} from './strategy/upload-strategy';\nimport {openUploadWindow} from '../utils/open-upload-window';\nimport {useDeleteFileEntries} from '@common/uploads/requests/delete-file-entries';\n\ninterface DeleteEntryProps {\n onSuccess: () => void;\n entryPath?: string;\n}\n\nexport function useActiveUpload() {\n const deleteFileEntries = useDeleteFileEntries();\n\n // use ref for setting ID to avoid extra renders, zustand selector\n // will pick up changed selector on first progress event\n const uploadIdRef = useRef();\n\n const uploadSingle = useFileUploadStore(s => s.uploadSingle);\n const _abortUpload = useFileUploadStore(s => s.abortUpload);\n const updateFileUpload = useFileUploadStore(s => s.updateFileUpload);\n const activeUpload = useFileUploadStore(s =>\n uploadIdRef.current ? s.fileUploads.get(uploadIdRef.current) : null,\n );\n\n const uploadFile = useCallback(\n (file: File | UploadedFile, config?: UploadStrategyConfig) => {\n uploadIdRef.current = uploadSingle(file, config);\n },\n [uploadSingle],\n );\n\n const selectAndUploadFile = useCallback(\n async (config?: UploadStrategyConfig) => {\n const files = await openUploadWindow({\n types: config?.restrictions?.allowedFileTypes,\n });\n uploadFile(files[0], config);\n return files[0];\n },\n [uploadFile],\n );\n\n const deleteEntry = useCallback(\n ({onSuccess, entryPath}: DeleteEntryProps) => {\n const handleSuccess = () => {\n if (activeUpload) {\n updateFileUpload(activeUpload.file.id, {\n ...activeUpload,\n entry: undefined,\n });\n }\n onSuccess();\n };\n\n if (!entryPath && !activeUpload?.entry?.id) {\n handleSuccess();\n return;\n }\n\n deleteFileEntries.mutate(\n {\n paths: entryPath ? [entryPath] : undefined,\n entryIds: activeUpload?.entry?.id\n ? [activeUpload?.entry?.id]\n : undefined,\n deleteForever: true,\n },\n {onSuccess: handleSuccess},\n );\n },\n [deleteFileEntries, activeUpload, updateFileUpload],\n );\n\n const abortUpload = useCallback(() => {\n if (activeUpload) {\n _abortUpload(activeUpload.file.id);\n }\n }, [activeUpload, _abortUpload]);\n\n return {\n uploadFile,\n selectAndUploadFile,\n percentage: activeUpload?.percentage || 0,\n uploadStatus: activeUpload?.status,\n entry: activeUpload?.entry,\n deleteEntry,\n isDeletingEntry: deleteFileEntries.isPending,\n activeUpload,\n abortUpload,\n };\n}\n","export interface UploadInputConfig {\n types?: (UploadInputType | string)[];\n extensions?: string[];\n multiple?: boolean;\n directory?: boolean;\n}\n\nexport enum UploadInputType {\n image = 'image/*',\n audio = 'audio/*',\n text = 'text/*',\n json = 'application/json',\n video = 'video/mp4,video/mpeg,video/x-m4v,video/*',\n}\n","import React, {CSSProperties, ReactNode, useId} from 'react';\nimport clsx from 'clsx';\nimport {InputSize} from '../forms/input-field/input-size';\nimport {getInputFieldClassNames} from '../forms/input-field/get-input-field-class-names';\nimport {useNumberFormatter} from '../../i18n/use-number-formatter';\nimport {clamp} from '../../utils/number/clamp';\n\nexport interface ProgressBarBaseProps {\n value?: number;\n minValue?: number;\n maxValue?: number;\n className?: string;\n showValueLabel?: boolean;\n size?: 'xs' | 'sm' | 'md';\n labelPosition?: 'top' | 'bottom';\n isIndeterminate?: boolean;\n label?: ReactNode;\n formatOptions?: Intl.NumberFormatOptions;\n role?: string;\n radius?: string;\n trackColor?: string;\n trackHeight?: string;\n progressColor?: string;\n}\n\nexport function ProgressBarBase(props: ProgressBarBaseProps) {\n let {\n value = 0,\n minValue = 0,\n maxValue = 100,\n size = 'md',\n label,\n showValueLabel = !!label,\n isIndeterminate = false,\n labelPosition = 'top',\n className,\n role,\n formatOptions = {\n style: 'percent',\n },\n radius = 'rounded',\n trackColor = 'bg-primary-light',\n progressColor = 'bg-primary',\n trackHeight = getSize(size),\n } = props;\n\n const id = useId();\n\n value = clamp(value, minValue, maxValue);\n\n const percentage = (value - minValue) / (maxValue - minValue);\n const formatter = useNumberFormatter(formatOptions);\n\n let valueLabel = '';\n if (!isIndeterminate && showValueLabel) {\n const valueToFormat =\n formatOptions.style === 'percent' ? percentage : value;\n valueLabel = formatter.format(valueToFormat);\n }\n\n const barStyle: CSSProperties = {};\n if (!isIndeterminate) {\n barStyle.width = `${Math.round(percentage * 100)}%`;\n }\n\n const style = getInputFieldClassNames({size});\n\n const labelEl = (label || valueLabel) && (\n
    \n {label && {label}}\n {valueLabel &&
    {valueLabel}
    }\n
    \n );\n\n return (\n \n {labelPosition === 'top' && labelEl}\n
    \n \n
    \n {labelPosition === 'bottom' && labelEl}\n \n );\n}\n\nfunction getSize(size: InputSize) {\n switch (size) {\n case 'sm':\n return 'h-6';\n case 'xs':\n return 'h-4';\n default:\n return 'h-8';\n }\n}\n","import React from 'react';\nimport {ProgressBarBase, ProgressBarBaseProps} from './progress-bar-base';\n\ninterface Props extends ProgressBarBaseProps {\n isIndeterminate?: boolean;\n}\n\nexport function ProgressBar(props: Props) {\n return ;\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const AddAPhotoIcon = createSvgIcon(\n \n, 'AddAPhotoOutlined');\n","import {createSvgIcon} from '@common/icons/create-svg-icon';\n\nexport const AvatarPlaceholderIcon = createSvgIcon(\n \n);\n","import React, {\n cloneElement,\n ComponentPropsWithRef,\n Fragment,\n JSXElementConstructor,\n ReactElement,\n ReactNode,\n useCallback,\n useId,\n useRef,\n} from 'react';\nimport clsx from 'clsx';\nimport {Button} from '../buttons/button';\nimport {Trans} from '../../i18n/trans';\nimport {useActiveUpload} from '../../uploads/uploader/use-active-upload';\nimport {UploadInputType} from '../../uploads/types/upload-input-config';\nimport {useController} from 'react-hook-form';\nimport {mergeProps} from '@react-aria/utils';\nimport {ProgressBar} from '../progress/progress-bar';\nimport {Disk} from '../../uploads/types/backend-metadata';\nimport {toast} from '@common/ui/toast/toast';\nimport {Field} from '@common/ui/forms/input-field/field';\nimport {\n getInputFieldClassNames,\n InputFieldStyle,\n} from '@common/ui/forms/input-field/get-input-field-class-names';\nimport {FileEntry} from '@common/uploads/file-entry';\nimport {useAutoFocus} from '@common/ui/focus/use-auto-focus';\nimport {UploadStrategyConfig} from '@common/uploads/uploader/strategy/upload-strategy';\nimport {SvgIconProps} from '@common/icons/svg-icon';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {AddAPhotoIcon} from '@common/icons/material/AddAPhoto';\nimport {AvatarPlaceholderIcon} from '@common/auth/ui/account-settings/avatar/avatar-placeholder-icon';\nimport {ButtonBaseProps} from '@common/ui/buttons/button-base';\n\nconst TwoMB = 2 * 1024 * 1024;\n\ninterface ImageSelectorProps {\n className?: string;\n label?: ReactNode;\n description?: ReactNode;\n invalid?: boolean;\n errorMessage?: ReactNode;\n required?: boolean;\n disabled?: boolean;\n value?: string;\n onChange?: (newValue: string) => void;\n defaultValue?: string;\n diskPrefix: string;\n showRemoveButton?: boolean;\n showEditButtonOnHover?: boolean;\n autoFocus?: boolean;\n variant?: 'input' | 'square' | 'avatar';\n placeholderIcon?: ReactElement;\n previewSize?: string;\n previewRadius?: string;\n stretchPreview?: boolean;\n}\nexport function ImageSelector({\n className,\n label,\n description,\n value,\n onChange,\n defaultValue,\n diskPrefix,\n showRemoveButton,\n showEditButtonOnHover = false,\n invalid,\n errorMessage,\n required,\n autoFocus,\n variant = 'input',\n previewSize = 'h-80',\n placeholderIcon,\n stretchPreview = false,\n previewRadius,\n disabled,\n}: ImageSelectorProps) {\n const {\n uploadFile,\n entry,\n uploadStatus,\n deleteEntry,\n isDeletingEntry,\n percentage,\n } = useActiveUpload();\n\n const inputRef = useRef(null);\n\n useAutoFocus({autoFocus}, inputRef);\n\n const fieldId = useId();\n const labelId = label ? `${fieldId}-label` : undefined;\n const descriptionId = description ? `${fieldId}-description` : undefined;\n\n const imageUrl = value || entry?.url;\n\n const uploadOptions: UploadStrategyConfig = {\n showToastOnRestrictionFail: true,\n restrictions: {\n allowedFileTypes: [UploadInputType.image],\n maxFileSize: TwoMB,\n },\n metadata: {\n diskPrefix,\n disk: Disk.public,\n },\n onSuccess: (entry: FileEntry) => {\n onChange?.(entry.url);\n },\n onError: message => {\n if (message) {\n toast.danger(message);\n }\n },\n };\n\n const inputFieldClassNames = getInputFieldClassNames({\n description,\n descriptionPosition: 'top',\n invalid,\n });\n\n let VariantElement: JSXElementConstructor;\n if (variant === 'avatar') {\n VariantElement = AvatarVariant;\n } else if (variant === 'square') {\n VariantElement = SquareVariant;\n } else {\n VariantElement = InputVariant;\n }\n\n const removeButton = showRemoveButton ? (\n {\n deleteEntry({\n onSuccess: () => onChange?.(''),\n });\n }}\n >\n \n \n ) : null;\n\n const useDefaultButton =\n defaultValue != null && value !== defaultValue ? (\n {\n onChange?.(defaultValue);\n }}\n >\n \n \n ) : null;\n\n const handleUpload = useCallback(() => {\n inputRef.current?.click();\n }, []);\n\n return (\n
    \n {label && (\n
    \n {label}\n
    \n )}\n {description && (\n
    {description}
    \n )}\n
    \n \n \n {\n if (e.target.files?.length) {\n uploadFile(e.target.files[0], uploadOptions);\n }\n }}\n />\n \n {uploadStatus === 'inProgress' && (\n \n )}\n \n
    \n
    \n );\n}\n\ninterface VariantProps {\n children: ReactElement>;\n inputFieldClassNames: InputFieldStyle;\n previewSize?: ImageSelectorProps['previewSize'];\n placeholderIcon?: ImageSelectorProps['placeholderIcon'];\n isLoading?: boolean;\n imageUrl?: string;\n removeButton?: ReactElement | null;\n useDefaultButton?: ReactElement | null;\n showEditButtonOnHover?: boolean;\n stretchPreview?: boolean;\n previewRadius?: string;\n handleUpload: () => void;\n disabled?: boolean;\n}\nfunction InputVariant({\n children,\n inputFieldClassNames,\n imageUrl,\n previewSize,\n stretchPreview,\n isLoading,\n handleUpload,\n removeButton,\n useDefaultButton,\n disabled,\n}: VariantProps) {\n if (imageUrl) {\n return (\n \n \n handleUpload()}\n src={imageUrl}\n alt=\"\"\n />\n {children}\n \n handleUpload()}\n disabled={isLoading || disabled}\n className=\"mr-10\"\n variant=\"outline\"\n color=\"primary\"\n size=\"xs\"\n >\n \n \n {removeButton && cloneElement(removeButton, {variant: 'outline'})}\n {useDefaultButton &&\n cloneElement(useDefaultButton, {variant: 'outline'})}\n \n );\n }\n return cloneElement(children, {\n className: clsx(\n inputFieldClassNames.input,\n 'py-8',\n 'file:bg-primary file:text-on-primary file:border-none file:rounded file:text-sm file:font-semibold file:px-10 file:h-24 file:mr-10',\n ),\n });\n}\n\nfunction SquareVariant({\n children,\n placeholderIcon,\n previewSize,\n imageUrl,\n stretchPreview,\n handleUpload,\n removeButton,\n useDefaultButton,\n previewRadius = 'rounded',\n showEditButtonOnHover = false,\n disabled,\n}: VariantProps) {\n return (\n
    \n handleUpload()}\n >\n {placeholderIcon &&\n !imageUrl &&\n cloneElement(placeholderIcon, {size: 'lg'})}\n \n {imageUrl ? (\n \n ) : (\n \n )}\n \n
    \n {children}\n {(removeButton || useDefaultButton) && (\n
    \n {removeButton && cloneElement(removeButton, {variant: 'link'})}\n {useDefaultButton &&\n cloneElement(useDefaultButton, {variant: 'link'})}\n
    \n )}\n \n );\n}\n\nfunction AvatarVariant({\n children,\n placeholderIcon,\n previewSize,\n isLoading,\n imageUrl,\n removeButton,\n useDefaultButton,\n handleUpload,\n previewRadius = 'rounded-full',\n disabled,\n}: VariantProps) {\n if (!placeholderIcon) {\n placeholderIcon = (\n \n );\n }\n return (\n
    \n handleUpload()}\n >\n {imageUrl ? (\n \n ) : (\n placeholderIcon\n )}\n
    \n \n \n \n
    \n
    \n {children}\n {(removeButton || useDefaultButton) && (\n
    \n {removeButton && cloneElement(removeButton, {variant: 'link'})}\n {useDefaultButton &&\n cloneElement(useDefaultButton, {variant: 'link'})}\n
    \n )}\n \n );\n}\n\ninterface FormImageSelectorProps extends ImageSelectorProps {\n name: string;\n}\nexport function FormImageSelector(props: FormImageSelectorProps) {\n const {\n field: {onChange, value = null},\n fieldState: {error},\n } = useController({\n name: props.name,\n });\n\n const formProps: Partial = {\n onChange,\n value,\n invalid: error != null,\n errorMessage: error ? : null,\n };\n\n return ;\n}\n","import {useForm} from 'react-hook-form';\nimport {useId} from 'react';\nimport {User} from '../../../user';\nimport {AccountSettingsPanel} from '../account-settings-panel';\nimport {Button} from '@common/ui/buttons/button';\nimport {Form} from '@common/ui/forms/form';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {useUpdateAccountDetails} from './update-account-details';\nimport {Trans} from '@common/i18n/trans';\nimport {useUploadAvatar} from '../avatar/upload-avatar';\nimport {useRemoveAvatar} from '../avatar/remove-avatar';\nimport {FormImageSelector} from '@common/ui/images/image-selector';\nimport {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';\nimport {AccountSettingsId} from '@common/auth/ui/account-settings/account-settings-sidenav';\n\ninterface Props {\n user: User;\n}\nexport function BasicInfoPanel({user}: Props) {\n const uploadAvatar = useUploadAvatar({user});\n const removeAvatar = useRemoveAvatar({user});\n const formId = useId();\n const form = useForm>>({\n defaultValues: {\n first_name: user.first_name || '',\n last_name: user.last_name || '',\n avatar: user.avatar,\n },\n });\n const updateDetails = useUpdateAccountDetails(form);\n\n return (\n }\n actions={\n \n \n \n }\n >\n {\n updateDetails.mutate(newDetails);\n }}\n id={formId}\n >\n
    \n }\n />\n }\n />\n
    \n \n }\n onChange={url => {\n if (url) {\n uploadAvatar.mutate({url});\n } else {\n removeAvatar.mutate();\n }\n }}\n />\n \n \n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {toast} from '@common/ui/toast/toast';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {message} from '@common/i18n/message';\nimport {apiClient} from '@common/http/query-client';\n\ninterface Response extends BackendResponse {}\n\nexport interface UpdatePasswordPayload {\n current_password: string;\n password: string;\n password_confirmation: string;\n}\n\nexport function useUpdatePassword(form: UseFormReturn) {\n return useMutation({\n mutationFn: (props: UpdatePasswordPayload) => updatePassword(props),\n onSuccess: () => {\n toast(message('Password changed'));\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction updatePassword(payload: UpdatePasswordPayload): Promise {\n return apiClient.put('auth/user/password', payload).then(r => r.data);\n}\n","import {useForm} from 'react-hook-form';\nimport {useId} from 'react';\nimport {Form} from '@common/ui/forms/form';\nimport {AccountSettingsPanel} from '@common/auth/ui/account-settings/account-settings-panel';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {UpdatePasswordPayload, useUpdatePassword} from './use-update-password';\nimport {Button} from '@common/ui/buttons/button';\nimport {Trans} from '@common/i18n/trans';\nimport {AccountSettingsId} from '@common/auth/ui/account-settings/account-settings-sidenav';\n\nexport function ChangePasswordPanel() {\n const form = useForm();\n const formId = useId();\n const updatePassword = useUpdatePassword(form);\n return (\n }\n actions={\n \n \n \n }\n >\n {\n updatePassword.mutate(newValues, {\n onSuccess: () => {\n form.reset();\n },\n });\n }}\n >\n }\n type=\"password\"\n autoComplete=\"current-password\"\n required\n />\n }\n type=\"password\"\n autoComplete=\"new-password\"\n required\n />\n }\n type=\"password\"\n autoComplete=\"new-password\"\n required\n />\n \n \n );\n}\n","import React, {ReactElement, useEffect, useRef, useState} from 'react';\nimport {SvgIconProps} from '@common/icons/svg-icon';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {useListboxContext} from '@common/ui/forms/listbox/listbox-context';\nimport {ProgressCircle} from '@common/ui/progress/progress-circle';\nimport {KeyboardArrowDownIcon} from '@common/icons/material/KeyboardArrowDown';\n\ninterface Props {\n isLoading?: boolean;\n icon?: ReactElement;\n}\nexport function ComboboxEndAdornment({isLoading, icon}: Props) {\n const timeout = useRef(null);\n const {trans} = useTrans();\n const [showLoading, setShowLoading] = useState(false);\n\n const {\n state: {isOpen, inputValue},\n } = useListboxContext();\n\n const lastInputValue = useRef(inputValue);\n useEffect(() => {\n if (isLoading && !showLoading) {\n if (timeout.current === null) {\n timeout.current = setTimeout(() => {\n setShowLoading(true);\n }, 500);\n }\n\n // If user is typing, clear the timer and restart since it is a new request\n if (inputValue !== lastInputValue.current) {\n clearTimeout(timeout.current);\n timeout.current = setTimeout(() => {\n setShowLoading(true);\n }, 500);\n }\n } else if (!isLoading) {\n // If loading is no longer happening, clear any timers and hide the loading circle\n setShowLoading(false);\n clearTimeout(timeout.current);\n timeout.current = null;\n }\n\n lastInputValue.current = inputValue;\n }, [isLoading, showLoading, inputValue]);\n\n // loading circle should only be displayed if menu is open, if menuTrigger is \"manual\", or first time load (to stop circle from showing up when user selects an option)\n const showLoadingIndicator = showLoading && (isOpen || isLoading);\n\n if (showLoadingIndicator) {\n return (\n \n );\n }\n\n return icon || ;\n}\n","import React, {ReactElement, Ref} from 'react';\nimport {BaseFieldPropsWithDom} from '../input-field/base-field-props';\nimport {Item} from '../listbox/item';\nimport {useListbox} from '../listbox/use-listbox';\nimport {IconButton} from '../../buttons/icon-button';\nimport {TextField} from '../input-field/text-field/text-field';\nimport {Listbox} from '../listbox/listbox';\nimport {SvgIconProps} from '@common/icons/svg-icon';\nimport {useListboxKeyboardNavigation} from '@common/ui/forms/listbox/use-listbox-keyboard-navigation';\nimport {createEventHandler} from '@common/utils/dom/create-event-handler';\nimport {ListBoxChildren, ListboxProps} from '../listbox/types';\nimport {Popover} from '../../overlays/popover';\nimport {ComboboxEndAdornment} from '@common/ui/forms/combobox/combobox-end-adornment';\n\nexport {Item as Option};\n\nexport type ComboboxProps = Omit<\n BaseFieldPropsWithDom,\n 'endAdornment'\n> &\n ListBoxChildren &\n ListboxProps & {\n selectionMode?: 'single' | 'none';\n isAsync?: boolean;\n isLoading?: boolean;\n openMenuOnFocus?: boolean;\n endAdornmentIcon?: ReactElement;\n useOptionLabelAsInputValue?: boolean;\n hideEndAdornment?: boolean;\n onEndAdornmentClick?: () => void;\n prependListbox?: boolean;\n listboxClassName?: string;\n };\n\nfunction ComboBox(\n props: ComboboxProps & {selectionMode: 'single'},\n ref: Ref\n) {\n const {\n children,\n items,\n isAsync,\n isLoading,\n openMenuOnFocus = true,\n endAdornmentIcon,\n onItemSelected,\n maxItems,\n clearInputOnItemSelection,\n inputValue: userInputValue,\n selectedValue,\n onSelectionChange,\n allowCustomValue = false,\n onInputValueChange,\n defaultInputValue,\n selectionMode = 'single',\n useOptionLabelAsInputValue,\n showEmptyMessage,\n floatingMaxHeight,\n hideEndAdornment = false,\n blurReferenceOnItemSelection,\n isOpen: propsIsOpen,\n onOpenChange: propsOnOpenChange,\n prependListbox,\n listboxClassName,\n onEndAdornmentClick,\n ...textFieldProps\n } = props;\n\n const listbox = useListbox(\n {\n ...props,\n floatingMaxHeight,\n blurReferenceOnItemSelection,\n selectionMode,\n role: 'listbox',\n virtualFocus: true,\n clearSelectionOnInputClear: true,\n },\n ref\n );\n\n const {\n reference,\n listboxId,\n onInputChange,\n state: {\n isOpen,\n setIsOpen,\n inputValue,\n setInputValue,\n selectValues,\n selectedValues,\n setActiveCollection,\n },\n collection,\n } = listbox;\n\n const textLabel = selectedValues[0]\n ? collection.get(selectedValues[0])?.textLabel\n : undefined;\n\n const {handleListboxSearchFieldKeydown} =\n useListboxKeyboardNavigation(listbox);\n\n const handleFocusAndClick = createEventHandler(\n (e: React.FocusEvent) => {\n if (openMenuOnFocus && !isOpen) {\n setIsOpen(true);\n }\n e.target.select();\n }\n );\n\n return (\n {\n // prevent focus from leaving input when scrolling listbox via mouse\n e.preventDefault();\n }}\n >\n {\n e.preventDefault();\n e.stopPropagation();\n if (onEndAdornmentClick) {\n onEndAdornmentClick();\n } else {\n setActiveCollection('all');\n setIsOpen(!isOpen);\n }\n }}\n >\n \n \n ) : null\n }\n aria-expanded={isOpen ? 'true' : 'false'}\n aria-haspopup=\"listbox\"\n aria-controls={isOpen ? listboxId : undefined}\n aria-autocomplete=\"list\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck=\"false\"\n onChange={onInputChange}\n value={useOptionLabelAsInputValue && textLabel ? textLabel : inputValue}\n onBlur={e => {\n if (allowCustomValue) {\n selectValues(e.target.value);\n } else if (!clearInputOnItemSelection) {\n const val = selectedValues[0];\n setInputValue(selectValues.length && val != null ? `${val}` : '');\n }\n }}\n onFocus={handleFocusAndClick}\n onClick={handleFocusAndClick}\n onKeyDown={e => handleListboxSearchFieldKeydown(e)}\n />\n \n );\n}\n\nconst ComboBoxForwardRef = React.forwardRef(ComboBox) as (\n props: ComboboxProps & {ref?: Ref}\n) => ReactElement;\nexport {ComboBoxForwardRef as ComboBox};\n","import React, {ReactElement, Ref, RefObject} from 'react';\nimport clsx from 'clsx';\nimport {useController} from 'react-hook-form';\nimport {mergeProps} from '@react-aria/utils';\nimport {getInputFieldClassNames} from '../input-field/get-input-field-class-names';\nimport {Field} from '../input-field/field';\nimport {BaseFieldPropsWithDom} from '../input-field/base-field-props';\nimport {useListbox} from '../listbox/use-listbox';\nimport {useField} from '../input-field/use-field';\nimport {Item} from '../listbox/item';\nimport {Section} from '../listbox/section';\nimport {Listbox} from '../listbox/listbox';\nimport {Trans} from '@common/i18n/trans';\nimport {useListboxKeyboardNavigation} from '../listbox/use-listbox-keyboard-navigation';\nimport {useTypeSelect} from '../listbox/use-type-select';\nimport {ListBoxChildren, ListboxProps} from '../listbox/types';\nimport {useIsMobileMediaQuery} from '@common/utils/hooks/is-mobile-media-query';\nimport {TextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {SearchIcon} from '@common/icons/material/Search';\nimport {ComboboxEndAdornment} from '@common/ui/forms/combobox/combobox-end-adornment';\n\nexport type SelectProps = Omit<\n BaseFieldPropsWithDom,\n 'value'\n> &\n ListboxProps &\n ListBoxChildren & {\n hideCaret?: boolean;\n selectionMode: 'single';\n minWidth?: string;\n searchPlaceholder?: string;\n showSearchField?: boolean;\n valueClassName?: string;\n };\nfunction Select(\n props: SelectProps,\n ref: Ref,\n) {\n const {\n hideCaret,\n placeholder = ,\n selectedValue,\n onItemSelected,\n onOpenChange,\n onInputValueChange,\n onSelectionChange,\n selectionMode,\n minWidth = 'min-w-128',\n children,\n searchPlaceholder,\n showEmptyMessage,\n showSearchField,\n defaultInputValue,\n inputValue: userInputValue,\n isLoading,\n isAsync,\n valueClassName,\n ...inputFieldProps\n } = props;\n\n const isMobile = useIsMobileMediaQuery();\n const listbox = useListbox(\n {\n ...props,\n clearInputOnItemSelection: true,\n showEmptyMessage: showEmptyMessage || showSearchField,\n floatingWidth: isMobile ? 'auto' : 'matchTrigger',\n selectionMode: 'single',\n role: 'listbox',\n virtualFocus: showSearchField,\n },\n ref,\n );\n const {\n state: {\n selectedValues,\n isOpen,\n setIsOpen,\n activeIndex,\n setSelectedIndex,\n inputValue,\n setInputValue,\n },\n collections,\n focusItem,\n listboxId,\n reference,\n refs,\n listContent,\n onInputChange,\n } = listbox;\n\n const {fieldProps, inputProps} = useField({\n ...inputFieldProps,\n focusRef: refs.reference as RefObject,\n });\n\n const selectedOption = collections.collection.get(selectedValues[0]);\n const content = selectedOption ? (\n \n {selectedOption.element.props.startIcon}\n \n {selectedOption.element.props.children}\n \n \n ) : (\n {placeholder}\n );\n\n const fieldClassNames = getInputFieldClassNames({\n ...props,\n endAdornment: true,\n });\n\n const {\n handleTriggerKeyDown,\n handleListboxKeyboardNavigation,\n handleListboxSearchFieldKeydown,\n } = useListboxKeyboardNavigation(listbox);\n\n const {findMatchingItem} = useTypeSelect();\n\n // focus matching item when user types, if dropdown is open\n const handleListboxTypeSelect = (e: React.KeyboardEvent) => {\n if (!isOpen) return;\n const i = findMatchingItem(e, listContent, activeIndex);\n if (i != null) {\n focusItem('increment', i);\n }\n };\n\n // select matching item when user types, if dropdown is closed\n const handleTriggerTypeSelect = (e: React.KeyboardEvent) => {\n if (isOpen) return undefined;\n const i = findMatchingItem(e, listContent, activeIndex);\n if (i != null) {\n setSelectedIndex(i);\n }\n };\n\n return (\n setInputValue('') : undefined}\n isLoading={isLoading}\n searchField={\n showSearchField && (\n }\n className=\"flex-shrink-0 px-8 pb-8 pt-4\"\n autoFocus\n aria-expanded={isOpen ? 'true' : 'false'}\n aria-haspopup=\"listbox\"\n aria-controls={isOpen ? listboxId : undefined}\n aria-autocomplete=\"list\"\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck=\"false\"\n value={inputValue}\n onChange={onInputChange}\n onKeyDown={e => {\n handleListboxSearchFieldKeydown(e);\n }}\n />\n )\n }\n >\n }\n >\n {\n setIsOpen(!isOpen);\n }}\n className={clsx(\n fieldClassNames.input,\n !fieldProps.unstyled && minWidth,\n )}\n >\n {content}\n \n
    \n \n );\n}\n\nconst SelectForwardRef = React.forwardRef(Select) as (\n props: SelectProps & {ref?: Ref},\n) => ReactElement;\nexport {SelectForwardRef as Select};\n\nexport type FormSelectProps = SelectProps & {\n name: string;\n};\nexport function FormSelect({\n children,\n ...props\n}: FormSelectProps) {\n const {\n field: {onChange, onBlur, value = null, ref},\n fieldState: {invalid, error},\n } = useController({\n name: props.name,\n });\n\n const formProps: Partial> = {\n onSelectionChange: onChange,\n onBlur,\n selectedValue: value,\n invalid,\n errorMessage: error?.message,\n name: props.name,\n };\n\n return (\n \n {children}\n \n );\n}\n\nexport {Item as Option};\nexport {Section as OptionGroup};\n","import {\n FormSelect,\n OptionGroup,\n SelectProps,\n} from '@common/ui/forms/select/select';\nimport {message} from '@common/i18n/message';\nimport {Option} from '@common/ui/forms/combobox/combobox';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {Timezone} from '@common/http/value-lists';\nimport {InputSize} from '@common/ui/forms/input-field/input-size';\nimport {ReactNode} from 'react';\n\ninterface Props extends Omit, 'selectionMode' | 'children'> {\n name: string;\n timezones: Record;\n size?: InputSize;\n label?: ReactNode;\n}\nexport function TimezoneSelect({\n name,\n size,\n timezones,\n label,\n ...selectProps\n}: Props) {\n const {trans} = useTrans();\n return (\n \n {Object.entries(timezones).map(([sectionName, sectionItems]) => (\n \n {sectionItems.map(timezone => (\n \n ))}\n \n ))}\n \n );\n}\n","import {useForm} from 'react-hook-form';\nimport {useId} from 'react';\nimport {Form} from '@common/ui/forms/form';\nimport {AccountSettingsPanel} from './account-settings-panel';\nimport {useUpdateAccountDetails} from './basic-info-panel/update-account-details';\nimport {Button} from '@common/ui/buttons/button';\nimport {User} from '../../user';\nimport {useValueLists} from '@common/http/value-lists';\nimport {Option} from '../../../ui/forms/combobox/combobox';\nimport {FormSelect} from '../../../ui/forms/select/select';\nimport {useChangeLocale} from '@common/i18n/change-locale';\nimport {Trans} from '@common/i18n/trans';\nimport {getLocalTimeZone} from '@internationalized/date';\nimport {AccountSettingsId} from '@common/auth/ui/account-settings/account-settings-sidenav';\nimport {message} from '@common/i18n/message';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {TimezoneSelect} from '@common/auth/ui/account-settings/timezone-select';\n\ninterface Props {\n user: User;\n}\nexport function LocalizationPanel({user}: Props) {\n const formId = useId();\n const {trans} = useTrans();\n const form = useForm>({\n defaultValues: {\n language: user.language || '',\n country: user.country || '',\n timezone: user.timezone || getLocalTimeZone(),\n },\n });\n const updateDetails = useUpdateAccountDetails(form);\n const changeLocale = useChangeLocale();\n const {data} = useValueLists(['timezones', 'countries', 'localizations']);\n\n const countries = data?.countries || [];\n const localizations = data?.localizations || [];\n const timezones = data?.timezones || {};\n\n return (\n }\n actions={\n \n \n \n }\n >\n {\n updateDetails.mutate(newDetails);\n changeLocale.mutate({locale: newDetails.language});\n }}\n id={formId}\n >\n }\n >\n {localizations.map(localization => (\n \n ))}\n \n }\n showSearchField\n searchPlaceholder={trans(message('Search countries'))}\n >\n {countries.map(country => (\n \n ))}\n \n }\n name=\"timezone\"\n timezones={timezones}\n />\n \n \n );\n}\n","import {DateFormatter} from '@internationalized/date';\nimport {useMemo, useRef} from 'react';\nimport {useSelectedLocale} from './selected-locale';\nimport {shallowEqual} from '../utils/shallow-equal';\n\nexport function useDateFormatter(\n options?: Intl.DateTimeFormatOptions\n): DateFormatter {\n // Reuse last options object if it is shallowly equal, which allows the useMemo result to also be reused.\n const lastOptions = useRef(\n null\n );\n if (\n options &&\n lastOptions.current &&\n shallowEqual(options as any, lastOptions.current)\n ) {\n options = lastOptions.current;\n }\n\n lastOptions.current = options;\n\n const {localeCode} = useSelectedLocale();\n return useMemo(\n () => new DateFormatter(localeCode, options),\n [localeCode, options]\n );\n}\n","import {DateValue, parseAbsoluteToLocal} from '@internationalized/date';\nimport {Fragment, memo} from 'react';\nimport {useDateFormatter} from './use-date-formatter';\nimport {shallowEqual} from '../utils/shallow-equal';\nimport {useSettings} from '../core/settings/use-settings';\nimport {useUserTimezone} from './use-user-timezone';\n\nexport const DateFormatPresets: Record<\n 'numeric' | 'short' | 'long',\n Intl.DateTimeFormatOptions\n> = {\n numeric: {year: 'numeric', month: '2-digit', day: '2-digit'},\n short: {year: 'numeric', month: 'short', day: '2-digit'},\n long: {month: 'long', day: '2-digit', year: 'numeric'},\n};\n\ninterface FormattedDateProps {\n date?: string | DateValue | Date;\n options?: Intl.DateTimeFormatOptions;\n preset?: keyof typeof DateFormatPresets;\n}\nexport const FormattedDate = memo(\n ({date, options, preset}: FormattedDateProps) => {\n const {dates} = useSettings();\n const timezone = useUserTimezone();\n const formatter = useDateFormatter(\n options ||\n (DateFormatPresets as Record)[\n preset || dates?.format\n ]\n );\n\n if (!date) {\n return null;\n }\n\n // make sure date with invalid format does not blow up the app\n try {\n if (typeof date === 'string') {\n date = parseAbsoluteToLocal(date).toDate();\n } else if ('toDate' in date) {\n date = date.toDate(timezone);\n }\n } catch (e) {\n return null;\n }\n\n return {formatter.format(date as Date)};\n },\n shallowEqual\n);\n","import React, {ReactNode} from 'react';\nimport {Button} from '../../buttons/button';\nimport {ErrorOutlineIcon} from '@common/icons/material/ErrorOutline';\nimport {DialogFooter} from './dialog-footer';\nimport {useDialogContext} from './dialog-context';\nimport {Dialog} from './dialog';\nimport {DialogHeader} from './dialog-header';\nimport {DialogBody} from './dialog-body';\nimport {Trans} from '@common/i18n/trans';\n\ninterface Props {\n className?: string;\n title: ReactNode;\n body: ReactNode;\n confirm: ReactNode;\n isDanger?: boolean;\n isLoading?: boolean;\n onConfirm?: () => void;\n}\nexport function ConfirmationDialog({\n className,\n title,\n body,\n confirm,\n isDanger,\n isLoading,\n onConfirm,\n}: Props) {\n const {close} = useDialogContext();\n return (\n \n }\n >\n {title}\n \n {body}\n \n {\n close(false);\n }}\n >\n \n \n {\n onConfirm?.();\n // if callback is passed in, caller is responsible for closing the dialog\n if (!onConfirm) {\n close(true);\n }\n }}\n >\n {confirm}\n \n \n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../../../../http/backend-response/backend-response';\nimport {toast} from '../../../../ui/toast/toast';\nimport {message} from '../../../../i18n/message';\nimport {apiClient} from '../../../../http/query-client';\nimport {showHttpErrorToast} from '../../../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\ninterface Props {\n id: number;\n}\n\nfunction deleteAccessToken({id}: Props): Promise {\n return apiClient.delete(`access-tokens/${id}`).then(r => r.data);\n}\n\nexport function useDeleteAccessToken() {\n return useMutation({\n mutationFn: (props: Props) => deleteAccessToken(props),\n onSuccess: () => {\n toast(message('Token deleted'));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {BackendResponse} from '../../../../http/backend-response/backend-response';\nimport {toast} from '../../../../ui/toast/toast';\nimport {AccessToken} from '../../../access-token';\nimport {onFormQueryError} from '../../../../errors/on-form-query-error';\nimport {message} from '../../../../i18n/message';\nimport {apiClient} from '../../../../http/query-client';\n\ninterface Response extends BackendResponse {\n token: AccessToken;\n plainTextToken: string;\n}\n\nexport interface CreateAccessTokenPayload {\n tokenName: string;\n}\n\nfunction createAccessToken(\n payload: CreateAccessTokenPayload,\n): Promise {\n return apiClient.post(`access-tokens`, payload).then(r => r.data);\n}\n\nexport function useCreateAccessToken(\n form: UseFormReturn,\n) {\n return useMutation({\n mutationFn: (props: CreateAccessTokenPayload) => createAccessToken(props),\n onSuccess: () => {\n toast(message('Token create'));\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n","import {useForm} from 'react-hook-form';\nimport {useState} from 'react';\nimport useClipboard from 'react-use-clipboard';\nimport {Dialog} from '../../../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../../../ui/overlays/dialog/dialog-header';\nimport {DialogBody} from '../../../../ui/overlays/dialog/dialog-body';\nimport {Form} from '../../../../ui/forms/form';\nimport {\n FormTextField,\n TextField,\n} from '../../../../ui/forms/input-field/text-field/text-field';\nimport {useDialogContext} from '../../../../ui/overlays/dialog/dialog-context';\nimport {DialogFooter} from '../../../../ui/overlays/dialog/dialog-footer';\nimport {Button} from '../../../../ui/buttons/button';\nimport {\n CreateAccessTokenPayload,\n useCreateAccessToken,\n} from './create-new-token';\nimport {ErrorIcon} from '../../../../icons/material/Error';\nimport {Trans} from '../../../../i18n/trans';\nimport {queryClient} from '@common/http/query-client';\n\nexport function CreateNewTokenDialog() {\n const form = useForm();\n const {formId, close} = useDialogContext();\n const createToken = useCreateAccessToken(form);\n\n const [plainTextToken, setPlainTextToken] = useState();\n\n const formNode = (\n {\n createToken.mutate(values, {\n onSuccess: response => {\n setPlainTextToken(response.plainTextToken);\n queryClient.invalidateQueries({queryKey: ['users']});\n },\n });\n }}\n >\n }\n required\n autoFocus\n />\n \n );\n\n return (\n \n \n \n \n \n {plainTextToken ? (\n \n ) : (\n formNode\n )}\n \n \n \n {!plainTextToken && (\n \n \n \n )}\n \n \n );\n}\n\ninterface PlainTextPreviewProps {\n plainTextToken: string;\n}\nfunction PlainTextPreview({plainTextToken}: PlainTextPreviewProps) {\n const [isCopied, copyToClipboard] = useClipboard(plainTextToken || '', {\n successDuration: 1000,\n });\n\n return (\n <>\n {\n e.currentTarget.focus();\n e.currentTarget.select();\n }}\n endAppend={\n \n }\n />\n
    \n \n \n
    \n \n );\n}\n","export default \"__VITE_ASSET__4046b921__\"","import {Link} from 'react-router-dom';\nimport clsx from 'clsx';\nimport {AccountSettingsPanel} from '../account-settings-panel';\nimport {User} from '../../../user';\nimport {IllustratedMessage} from '../../../../ui/images/illustrated-message';\nimport {SvgImage} from '../../../../ui/images/svg-image/svg-image';\nimport {Button} from '../../../../ui/buttons/button';\nimport {FormattedDate} from '../../../../i18n/formatted-date';\nimport {AccessToken} from '../../../access-token';\nimport {DialogTrigger} from '../../../../ui/overlays/dialog/dialog-trigger';\nimport {ConfirmationDialog} from '../../../../ui/overlays/dialog/confirmation-dialog';\nimport {useDeleteAccessToken} from './delete-access-token';\nimport {CreateNewTokenDialog} from './create-new-token-dialog';\nimport {LinkStyle} from '../../../../ui/buttons/external-link';\nimport {useAuth} from '../../../use-auth';\nimport {Trans} from '../../../../i18n/trans';\nimport secureFilesSvg from './secure-files.svg';\nimport {useSettings} from '../../../../core/settings/use-settings';\nimport {queryClient} from '@common/http/query-client';\nimport {AccountSettingsId} from '@common/auth/ui/account-settings/account-settings-sidenav';\n\ninterface Props {\n user: User;\n}\nexport function AccessTokenPanel({user}: Props) {\n const tokens = user.tokens || [];\n const {hasPermission} = useAuth();\n const {api} = useSettings();\n if (!api?.integrated || !hasPermission('api.access')) return null;\n return (\n }\n titleSuffix={\n \n \n \n }\n actions={}\n >\n {!tokens.length ? (\n }\n title={}\n />\n ) : (\n tokens.map((token, index) => (\n \n ))\n )}\n \n );\n}\n\ninterface TokenLineProps {\n token: AccessToken;\n isLast: boolean;\n}\nfunction TokenLine({token, isLast}: TokenLineProps) {\n return (\n \n
    \n
    \n \n
    \n
    {token.name}
    \n
    \n \n
    \n
    \n {token.last_used_at ? (\n \n ) : (\n \n )}\n
    \n
    \n \n \n );\n}\n\nfunction CreateNewTokenButton() {\n return (\n \n \n \n \n );\n}\n\ninterface DeleteTokenButtonProps {\n token: AccessToken;\n}\nfunction DeleteTokenButton({token}: DeleteTokenButtonProps) {\n const deleteToken = useDeleteAccessToken();\n return (\n {\n if (isConfirmed) {\n deleteToken.mutate(\n {id: token.id},\n {\n onSuccess: () =>\n queryClient.invalidateQueries({queryKey: ['users']}),\n },\n );\n }\n }}\n >\n \n \n \n }\n body={\n \n }\n confirm={}\n />\n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '../../../../http/backend-response/backend-response';\nimport {useLogout} from '../../../requests/logout';\nimport {toast} from '../../../../ui/toast/toast';\nimport {useAuth} from '../../../use-auth';\nimport {apiClient} from '../../../../http/query-client';\nimport {showHttpErrorToast} from '../../../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\nfunction deleteAccount(userId: number): Promise {\n return apiClient\n .delete(`users/${userId}`, {params: {deleteCurrentUser: true}})\n .then(r => r.data);\n}\n\nexport function useDeleteAccount() {\n const {user} = useAuth();\n const logout = useLogout();\n return useMutation({\n mutationFn: () => deleteAccount(user!.id),\n onSuccess: () => {\n toast('Account deleted');\n logout.mutate();\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n","import {AccountSettingsPanel} from '../account-settings-panel';\nimport {Button} from '@common/ui/buttons/button';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';\nimport {useDeleteAccount} from './delete-account';\nimport {Trans} from '@common/i18n/trans';\nimport {AccountSettingsId} from '@common/auth/ui/account-settings/account-settings-sidenav';\n\nexport function DangerZonePanel() {\n const deleteAccount = useDeleteAccount();\n\n return (\n }\n >\n {\n if (isConfirmed) {\n deleteAccount.mutate();\n }\n }}\n >\n \n }\n body={\n \n }\n confirm={}\n />\n \n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\nexport function useEnableTwoFactor() {\n return useMutation({\n mutationFn: enable,\n onError: r => showHttpErrorToast(r),\n });\n}\n\nfunction enable(): Promise {\n return apiClient\n .post('auth/user/two-factor-authentication')\n .then(response => response.data);\n}\n","import {Fragment, ReactNode} from 'react';\nimport {Trans} from '@common/i18n/trans';\n\ninterface Props {\n title: ReactNode;\n subtitle?: ReactNode;\n description?: ReactNode;\n actions?: ReactNode;\n children?: ReactNode;\n}\nexport function TwoFactorStepperLayout({\n title,\n subtitle,\n description,\n actions,\n children,\n}: Props) {\n if (!subtitle) {\n subtitle = (\n \n );\n }\n return (\n \n
    {title}
    \n
    {subtitle}
    \n

    {description}

    \n {children}\n
    {actions}
    \n
    \n );\n}\n","import {useQuery} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient, queryClient} from '@common/http/query-client';\n\ninterface Response extends BackendResponse {\n confirmed: boolean;\n}\n\nexport function usePasswordConfirmationStatus() {\n return useQuery({\n queryKey: ['password-confirmation-status'],\n queryFn: () => fetchStatus(),\n });\n}\n\nfunction fetchStatus(): Promise {\n return apiClient\n .get('auth/user/confirmed-password-status', {params: {seconds: 9000}})\n .then(response => response.data);\n}\n\nexport function setPasswordConfirmationStatus(confirmed: boolean) {\n queryClient.setQueryData(['password-confirmation-status'], {confirmed});\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\nimport {UseFormReturn} from 'react-hook-form';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\n\ninterface Response extends BackendResponse {}\n\nexport interface ConfirmPasswordPayload {\n password: string;\n}\n\nexport function useConfirmPassword(\n form: UseFormReturn,\n) {\n return useMutation({\n mutationFn: (payload: ConfirmPasswordPayload) => confirm(payload),\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction confirm(payload: ConfirmPasswordPayload): Promise {\n return apiClient\n .post('auth/user/confirm-password', payload)\n .then(response => response.data);\n}\n","import {Dialog} from '@common/ui/overlays/dialog/dialog';\nimport {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';\nimport {\n ConfirmPasswordPayload,\n useConfirmPassword,\n} from '@common/auth/ui/confirm-password/requests/use-confirm-password';\nimport {useForm} from 'react-hook-form';\nimport {DialogHeader} from '@common/ui/overlays/dialog/dialog-header';\nimport {Trans} from '@common/i18n/trans';\nimport {DialogBody} from '@common/ui/overlays/dialog/dialog-body';\nimport {Form} from '@common/ui/forms/form';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {DialogFooter} from '@common/ui/overlays/dialog/dialog-footer';\nimport {Button} from '@common/ui/buttons/button';\n\nexport function ConfirmPasswordDialog() {\n const {close, formId} = useDialogContext();\n const form = useForm();\n const confirmPassword = useConfirmPassword(form);\n return (\n \n \n \n \n \n

    \n \n

    \n \n confirmPassword.mutate(values, {\n onSuccess: () => close(values.password),\n })\n }\n >\n }\n type=\"password\"\n required\n autoFocus\n />\n \n
    \n \n \n \n \n \n \n
    \n );\n}\n","import {\n setPasswordConfirmationStatus,\n usePasswordConfirmationStatus,\n} from '@common/auth/ui/confirm-password/requests/use-password-confirmation-status';\nimport {openDialog} from '@common/ui/overlays/store/dialog-store';\nimport {ConfirmPasswordDialog} from '@common/auth/ui/confirm-password/confirm-password-dialog';\nimport {useCallback, useRef} from 'react';\n\ninterface Props {\n needsPassword?: boolean;\n}\nexport function usePasswordConfirmedAction({needsPassword}: Props = {}) {\n const {data, isLoading} = usePasswordConfirmationStatus();\n const passwordRef = useRef();\n\n const withConfirmedPassword = useCallback(\n async (action: (password?: string) => void) => {\n if (data?.confirmed && (passwordRef.current || !needsPassword)) {\n action(passwordRef.current);\n } else {\n const password = await openDialog(ConfirmPasswordDialog);\n if (password) {\n passwordRef.current = password;\n setPasswordConfirmationStatus(true);\n action(passwordRef.current);\n }\n }\n },\n [data?.confirmed, needsPassword]\n );\n\n return {\n isLoading,\n withConfirmedPassword,\n };\n}\n","import {useEnableTwoFactor} from '@common/auth/ui/two-factor/requests/use-enable-two-factor';\nimport {TwoFactorStepperLayout} from '@common/auth/ui/two-factor/stepper/two-factor-stepper-layout';\nimport {Trans} from '@common/i18n/trans';\nimport {Button} from '@common/ui/buttons/button';\nimport {usePasswordConfirmedAction} from '@common/auth/ui/confirm-password/use-password-confirmed-action';\n\ninterface Props {\n onEnabled: () => void;\n}\nexport function TwoFactorDisabledStep({onEnabled}: Props) {\n const enableTwoFactor = useEnableTwoFactor();\n const {withConfirmedPassword, isLoading: confirmPasswordIsLoading} =\n usePasswordConfirmedAction();\n const isLoading = enableTwoFactor.isPending || confirmPasswordIsLoading;\n\n return (\n \n }\n actions={\n {\n withConfirmedPassword(() => {\n enableTwoFactor.mutate(undefined, {\n onSuccess: onEnabled,\n });\n });\n }}\n >\n \n \n }\n />\n );\n}\n","import {useQuery} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\n\ninterface Response extends BackendResponse {\n svg: string;\n secret: string;\n}\n\nexport function useTwoFactorQrCode() {\n return useQuery({\n queryKey: ['two-factor-qr-code'],\n queryFn: () => fetchCode(),\n });\n}\n\nfunction fetchCode(): Promise {\n return apiClient\n .get('auth/user/two-factor/qr-code')\n .then(response => response.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\nimport {UseFormReturn} from 'react-hook-form';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\n\ninterface Response extends BackendResponse {}\n\nexport interface ConfirmTwoFactorPayload {\n code: string;\n}\n\nexport function useConfirmTwoFactor(\n form: UseFormReturn,\n) {\n return useMutation({\n mutationFn: (payload: ConfirmTwoFactorPayload) => confirm(payload),\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction confirm(payload: ConfirmTwoFactorPayload): Promise {\n return apiClient\n .post('auth/user/confirmed-two-factor-authentication', payload)\n .then(response => response.data);\n}\n","import clsx from 'clsx';\n\ninterface SkeletonProps {\n variant?: 'avatar' | 'text' | 'rect' | 'icon';\n animation?: 'pulsate' | 'wave' | null; // disable animation completely with null\n className?: string;\n size?: string;\n display?: string;\n radius?: string;\n}\nexport function Skeleton({\n variant = 'text',\n animation = 'wave',\n size,\n className,\n display = 'block',\n radius = 'rounded',\n}: SkeletonProps) {\n return (\n \n );\n}\n\ninterface SkeletonSizeProps {\n variant: SkeletonProps['variant'];\n size: SkeletonProps['size'];\n}\nfunction skeletonSize({variant, size}: SkeletonSizeProps): string | undefined {\n if (size) {\n return size;\n }\n\n switch (variant) {\n case 'avatar':\n return 'h-40 w-40';\n case 'icon':\n return 'h-24 h-24';\n case 'rect':\n return 'h-full w-full';\n default:\n return 'w-full';\n }\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\nexport function useDisableTwoFactor() {\n return useMutation({\n mutationFn: disable,\n onError: r => showHttpErrorToast(r),\n });\n}\n\nfunction disable(): Promise {\n return apiClient\n .delete('auth/user/two-factor-authentication')\n .then(response => response.data);\n}\n","import {AnimatePresence, m} from 'framer-motion';\nimport {opacityAnimation} from '@common/ui/animation/opacity-animation';\nimport {ReactNode} from 'react';\nimport {useTwoFactorQrCode} from '@common/auth/ui/two-factor/requests/use-two-factor-qr-code';\nimport {useForm} from 'react-hook-form';\nimport {\n ConfirmTwoFactorPayload,\n useConfirmTwoFactor,\n} from '@common/auth/ui/two-factor/requests/use-confirm-two-factor';\nimport {TwoFactorStepperLayout} from '@common/auth/ui/two-factor/stepper/two-factor-stepper-layout';\nimport {Trans} from '@common/i18n/trans';\nimport {Skeleton} from '@common/ui/skeleton/skeleton';\nimport {Form} from '@common/ui/forms/form';\nimport {queryClient} from '@common/http/query-client';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {useDisableTwoFactor} from '@common/auth/ui/two-factor/requests/use-disable-two-factor';\nimport {usePasswordConfirmedAction} from '@common/auth/ui/confirm-password/use-password-confirmed-action';\nimport {Button} from '@common/ui/buttons/button';\n\ninterface Props {\n onCancel: () => void;\n onConfirmed: () => void;\n}\nexport function TwoFactorConfirmationStep(props: Props) {\n const {data} = useTwoFactorQrCode();\n\n return (\n }\n description={\n \n }\n >\n \n {!data ? (\n }\n secret={}\n />\n ) : (\n }\n secret={\n \n }\n />\n )}\n \n \n \n );\n}\n\nfunction CodeForm({onCancel, onConfirmed}: Props) {\n const form = useForm();\n const confirmTwoFactor = useConfirmTwoFactor(form);\n const disableTwoFactor = useDisableTwoFactor();\n const {withConfirmedPassword, isLoading: confirmPasswordIsLoading} =\n usePasswordConfirmedAction();\n const isLoading =\n confirmTwoFactor.isPending ||\n disableTwoFactor.isPending ||\n confirmPasswordIsLoading;\n\n return (\n \n withConfirmedPassword(() => {\n confirmTwoFactor.mutate(values, {\n onSuccess: () => {\n queryClient.invalidateQueries({queryKey: ['users']});\n onConfirmed();\n },\n });\n })\n }\n >\n }\n autoFocus\n />\n
    \n {\n withConfirmedPassword(() => {\n disableTwoFactor.mutate(undefined, {onSuccess: onCancel});\n });\n }}\n >\n \n \n \n \n \n
    \n \n );\n}\n\ninterface QrCodeLayoutProps {\n animationKey: string;\n svg: ReactNode;\n secret: ReactNode;\n}\nfunction QrCodeLayout({animationKey, svg, secret}: QrCodeLayoutProps) {\n return (\n \n
    {svg}
    \n
    {secret}
    \n
    \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\nexport function useRegenerateTwoFactorCodes() {\n return useMutation({\n mutationFn: () => regenerate(),\n onError: r => showHttpErrorToast(r),\n });\n}\n\nfunction regenerate(): Promise {\n return apiClient\n .post('auth/user/two-factor-recovery-codes')\n .then(response => response.data);\n}\n","import {User} from '@common/auth/user';\nimport {useDisableTwoFactor} from '@common/auth/ui/two-factor/requests/use-disable-two-factor';\nimport {useRegenerateTwoFactorCodes} from '@common/auth/ui/two-factor/requests/use-regenerate-two-factor-codes';\nimport {Fragment} from 'react';\nimport {queryClient} from '@common/http/query-client';\nimport {Trans} from '@common/i18n/trans';\nimport {TwoFactorStepperLayout} from '@common/auth/ui/two-factor/stepper/two-factor-stepper-layout';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\nimport {usePasswordConfirmedAction} from '@common/auth/ui/confirm-password/use-password-confirmed-action';\nimport {Button} from '@common/ui/buttons/button';\n\ninterface Props {\n user: User;\n onDisabled: () => void;\n}\nexport function TwoFactorEnabledStep({user, onDisabled}: Props) {\n const disableTwoFactor = useDisableTwoFactor();\n const regenerateCodes = useRegenerateTwoFactorCodes();\n const {withConfirmedPassword, isLoading: confirmPasswordIsLoading} =\n usePasswordConfirmedAction();\n const isLoading =\n disableTwoFactor.isPending ||\n regenerateCodes.isPending ||\n confirmPasswordIsLoading;\n\n const actions = (\n \n \n withConfirmedPassword(() => {\n regenerateCodes.mutate(undefined, {\n onSuccess: () => {\n queryClient.invalidateQueries({queryKey: ['users']});\n },\n });\n })\n }\n variant=\"outline\"\n disabled={isLoading}\n className=\"mr-12\"\n >\n \n \n {\n withConfirmedPassword(() => {\n disableTwoFactor.mutate(undefined, {\n onSuccess: () => {\n toast(message('Two factor authentication has been disabled.'));\n onDisabled();\n },\n });\n });\n }}\n >\n \n \n \n );\n\n return (\n }\n description={\n \n }\n actions={actions}\n >\n
    \n {user.two_factor_recovery_codes?.map(code => (\n
    \n {code}\n
    \n ))}\n
    \n \n );\n}\n","import {useState} from 'react';\nimport {User} from '@common/auth/user';\nimport {TwoFactorDisabledStep} from '@common/auth/ui/two-factor/stepper/two-factor-disabled-step';\nimport {TwoFactorConfirmationStep} from '@common/auth/ui/two-factor/stepper/two-factor-confirmation-step';\nimport {TwoFactorEnabledStep} from '@common/auth/ui/two-factor/stepper/two-factor-enabled-step';\n\nenum Status {\n Disabled,\n WaitingForConfirmation,\n Enabled,\n}\n\ninterface Props {\n user: User;\n}\nexport function TwoFactorStepper({user}: Props) {\n const [status, setStatus] = useState(getStatus(user));\n switch (status) {\n case Status.Disabled:\n return (\n setStatus(Status.WaitingForConfirmation)}\n />\n );\n case Status.WaitingForConfirmation:\n return (\n {\n setStatus(Status.Disabled);\n }}\n onConfirmed={() => {\n setStatus(Status.Enabled);\n }}\n />\n );\n case Status.Enabled:\n return (\n setStatus(Status.Disabled)}\n />\n );\n }\n}\n\nfunction getStatus(user: User): Status {\n if (user.two_factor_confirmed_at) {\n return Status.Enabled;\n } else if (user.two_factor_recovery_codes) {\n return Status.WaitingForConfirmation;\n }\n return Status.Disabled;\n}\n","import {useQuery} from '@tanstack/react-query';\nimport {apiClient} from '@common/http/query-client';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\n\nexport interface ActiveSession {\n id: string;\n platform?: string;\n device_type?: 'mobile' | 'tablet' | 'desktop';\n browser?: string;\n country?: string;\n city?: string;\n ip_address?: string;\n token?: string;\n is_current_device: boolean;\n last_active: string;\n created_at: string;\n}\n\ninterface Response extends BackendResponse {\n sessions: ActiveSession[];\n}\n\nexport function useUserSessions() {\n return useQuery({\n queryKey: ['user-sessions'],\n queryFn: () => fetchUserSessions(),\n });\n}\n\nfunction fetchUserSessions() {\n return apiClient\n .get(`user-sessions`)\n .then(response => response.data);\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const ComputerIcon = createSvgIcon(\n \n, 'ComputerOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const SmartphoneIcon = createSvgIcon(\n \n, 'SmartphoneOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const TabletIcon = createSvgIcon(\n \n, 'TabletOutlined');\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\ninterface Payload {\n password: string;\n}\n\nexport function useLogoutOtherSessions() {\n return useMutation({\n mutationFn: (payload: Payload) => logoutOther(payload),\n onError: r => showHttpErrorToast(r),\n });\n}\n\nfunction logoutOther(payload: Payload): Promise {\n return apiClient\n .post('user-sessions/logout-other', payload)\n .then(response => response.data);\n}\n","import {User} from '../../../user';\nimport {AccountSettingsPanel} from '../account-settings-panel';\nimport {Trans} from '@common/i18n/trans';\nimport {AccountSettingsId} from '@common/auth/ui/account-settings/account-settings-sidenav';\nimport {\n ActiveSession,\n useUserSessions,\n} from '@common/auth/ui/account-settings/sessions-panel/requests/use-user-sessions';\nimport {ComputerIcon} from '@common/icons/material/Computer';\nimport {SmartphoneIcon} from '@common/icons/material/Smartphone';\nimport {TabletIcon} from '@common/icons/material/Tablet';\nimport {FormattedRelativeTime} from '@common/i18n/formatted-relative-time';\nimport {SvgIconProps} from '@common/icons/svg-icon';\nimport {Fragment, ReactNode} from 'react';\nimport {ProgressCircle} from '@common/ui/progress/progress-circle';\nimport {useLogoutOtherSessions} from '@common/auth/ui/account-settings/sessions-panel/requests/use-logout-other-sessions';\nimport {usePasswordConfirmedAction} from '@common/auth/ui/confirm-password/use-password-confirmed-action';\nimport {Button} from '@common/ui/buttons/button';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\n\ninterface Props {\n user: User;\n}\nexport function SessionsPanel({user}: Props) {\n const {data, isLoading} = useUserSessions();\n const logoutOther = useLogoutOtherSessions();\n const {withConfirmedPassword, isLoading: checkingPasswordStatus} =\n usePasswordConfirmedAction({needsPassword: true});\n\n const sessionList = (\n
    \n {data?.sessions?.map(session => (\n \n ))}\n
    \n );\n\n return (\n }\n >\n

    \n \n

    \n
    \n {isLoading ? (\n
    \n \n
    \n ) : (\n sessionList\n )}\n
    \n {\n withConfirmedPassword(password => {\n logoutOther.mutate(\n {password: password!},\n {\n onSuccess: () => {\n toast(message('Logged out other sessions.'));\n },\n },\n );\n });\n }}\n >\n \n \n \n );\n}\n\ninterface SessionItemProps {\n session: ActiveSession;\n}\nfunction SessionItem({session}: SessionItemProps) {\n return (\n
    \n
    \n \n
    \n
    \n
    \n {session.platform} -{' '}\n {session.browser}\n
    \n
    \n {session.city}, {session.country}\n
    \n
    \n - \n
    \n
    \n
    \n );\n}\n\ninterface DeviceIconProps {\n device: ActiveSession['device_type'];\n size: SvgIconProps['size'];\n}\nfunction DeviceIcon({device, size}: DeviceIconProps) {\n switch (device) {\n case 'mobile':\n return ;\n case 'tablet':\n return ;\n default:\n return ;\n }\n}\n\ninterface LastActiveProps {\n session: ActiveSession;\n}\nfunction LastActive({session}: LastActiveProps) {\n if (session.is_current_device) {\n return (\n \n \n \n );\n }\n\n return ;\n}\n\ninterface IpAddressProps {\n session: ActiveSession;\n}\nfunction IpAddress({session}: IpAddressProps) {\n if (session.ip_address) {\n return {session.ip_address};\n } else if (session.token) {\n return ;\n }\n return ;\n}\n\ninterface ValueOrUnknownProps {\n children: ReactNode;\n}\nfunction ValueOrUnknown({children}: ValueOrUnknownProps) {\n return children ? (\n {children}\n ) : (\n \n );\n}\n","import {Navbar} from '@common/ui/navigation/navbar/navbar';\nimport {useUser} from '../use-user';\nimport {ProgressCircle} from '@common/ui/progress/progress-circle';\nimport {SocialLoginPanel} from './social-login-panel';\nimport {BasicInfoPanel} from './basic-info-panel/basic-info-panel';\nimport {ChangePasswordPanel} from './change-password-panel/change-password-panel';\nimport {LocalizationPanel} from './localization-panel';\nimport {AccessTokenPanel} from './access-token-panel/access-token-panel';\nimport {DangerZonePanel} from './danger-zone-panel/danger-zone-panel';\nimport {Trans} from '@common/i18n/trans';\nimport {StaticPageTitle} from '@common/seo/static-page-title';\nimport {AccountSettingsPanel} from '@common/auth/ui/account-settings/account-settings-panel';\nimport {TwoFactorStepper} from '@common/auth/ui/two-factor/stepper/two-factor-auth-stepper';\nimport {\n AccountSettingsId,\n AccountSettingsSidenav,\n} from '@common/auth/ui/account-settings/account-settings-sidenav';\nimport {SessionsPanel} from '@common/auth/ui/account-settings/sessions-panel/sessions-panel';\nimport {useContext} from 'react';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\n\nexport function AccountSettingsPage() {\n const {auth} = useContext(SiteConfigContext);\n const {data, isLoading} = useUser('me', {\n with: ['roles', 'social_profiles', 'tokens'],\n });\n return (\n
    \n \n \n \n \n
    \n
    \n

    \n \n

    \n
    \n \n
    \n {isLoading || !data ? (\n \n ) : (\n
    \n \n
    \n {auth.accountSettingsPanels?.map(panel => (\n \n ))}\n \n \n \n }\n >\n
    \n \n
    \n \n \n \n \n \n
    \n
    \n )}\n
    \n
    \n
    \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {onFormQueryError} from '../../errors/on-form-query-error';\nimport {toast} from '../../ui/toast/toast';\nimport {useNavigate} from '../../utils/hooks/use-navigate';\nimport {apiClient} from '../../http/query-client';\n\ninterface Response extends BackendResponse {\n message: string;\n}\n\nexport interface SendPasswordResetEmailPayload {\n email: string;\n}\n\nexport function useSendPasswordResetEmail(\n form: UseFormReturn,\n) {\n const navigate = useNavigate();\n return useMutation({\n mutationFn: sendResetPasswordEmail,\n onSuccess: response => {\n toast(response.message);\n navigate('/login');\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction sendResetPasswordEmail(\n payload: SendPasswordResetEmailPayload,\n): Promise {\n return apiClient\n .post('auth/forgot-password', payload)\n .then(response => response.data);\n}\n","import {Link, useSearchParams} from 'react-router-dom';\nimport {useForm} from 'react-hook-form';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {Button} from '../../ui/buttons/button';\nimport {Form} from '../../ui/forms/form';\nimport {LinkStyle} from '../../ui/buttons/external-link';\nimport {AuthLayout} from './auth-layout/auth-layout';\nimport {\n SendPasswordResetEmailPayload,\n useSendPasswordResetEmail,\n} from '../requests/send-reset-password-email';\nimport {Trans} from '../../i18n/trans';\nimport {StaticPageTitle} from '../../seo/static-page-title';\nimport {useSettings} from '../../core/settings/use-settings';\n\nexport function ForgotPasswordPage() {\n const {registration} = useSettings();\n\n const [searchParams] = useSearchParams();\n const searchParamsEmail = searchParams.get('email') || undefined;\n\n const form = useForm({\n defaultValues: {email: searchParamsEmail},\n });\n const sendEmail = useSendPasswordResetEmail(form);\n\n const message = !registration.disable && (\n (\n \n {parts}\n \n ),\n }}\n message=\"Don't have an account? Sign up.\"\n />\n );\n\n return (\n \n \n \n \n {\n sendEmail.mutate(payload);\n }}\n >\n
    \n \n
    \n }\n required\n />\n \n \n \n \n
    \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {onFormQueryError} from '../../errors/on-form-query-error';\nimport {toast} from '../../ui/toast/toast';\nimport {message} from '../../i18n/message';\nimport {useNavigate} from '../../utils/hooks/use-navigate';\nimport {apiClient} from '../../http/query-client';\n\ninterface Response extends BackendResponse {\n bootstrapData: string;\n}\n\nexport interface ResetPasswordPayload {\n email: string;\n password: string;\n password_confirmation: string;\n token: string;\n}\n\nfunction reset(payload: ResetPasswordPayload): Promise {\n return apiClient\n .post('auth/reset-password', payload)\n .then(response => response.data);\n}\n\nexport function useResetPassword(form: UseFormReturn) {\n const navigate = useNavigate();\n return useMutation({\n mutationFn: reset,\n onSuccess: () => {\n navigate('/login', {replace: true});\n toast(message('Your password has been reset!'));\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n","import {Link, useParams} from 'react-router-dom';\nimport {useForm} from 'react-hook-form';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {Button} from '../../ui/buttons/button';\nimport {Form} from '../../ui/forms/form';\nimport {LinkStyle} from '../../ui/buttons/external-link';\nimport {AuthLayout} from './auth-layout/auth-layout';\nimport {\n ResetPasswordPayload,\n useResetPassword,\n} from '../requests/reset-password';\nimport {Trans} from '../../i18n/trans';\nimport {StaticPageTitle} from '../../seo/static-page-title';\n\nexport function ResetPasswordPage() {\n const {token} = useParams();\n const form = useForm({defaultValues: {token}});\n const resetPassword = useResetPassword(form);\n\n const heading = ;\n\n const message = (\n (\n \n {parts}\n \n ),\n }}\n message=\"Don't have an account? Sign up.\"\n />\n );\n\n return (\n \n \n \n \n {\n resetPassword.mutate(payload);\n }}\n >\n }\n required\n />\n }\n required\n />\n }\n required\n />\n \n \n \n \n \n );\n}\n","import {RegisterPage} from './ui/register-page';\nimport {AuthRoute} from './guards/auth-route';\nimport {AccountSettingsPage} from './ui/account-settings/account-settings-page';\nimport {GuestRoute} from './guards/guest-route';\nimport {ForgotPasswordPage} from './ui/forgot-password-page';\nimport {ResetPasswordPage} from './ui/reset-password-page';\nimport React, {Fragment} from 'react';\nimport {Route} from 'react-router-dom';\nimport {LoginPageWrapper} from '@common/auth/ui/login-page-wrapper';\n\nexport const AuthRoutes = (\n \n } />\n \n \n \n }\n />\n \n \n \n }\n />\n \n \n \n }\n />\n \n \n \n }\n />\n \n \n \n }\n />\n \n \n \n }\n />\n \n);\n","import {useQuery} from '@tanstack/react-query';\nimport {apiClient} from '../../http/query-client';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {PaginatedBackendResponse} from '../../http/backend-response/pagination-response';\nimport {Product} from '../product';\nimport {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';\n\nconst endpoint = 'billing/products';\n\nexport interface FetchProductsResponse extends BackendResponse {\n products: Product[];\n}\n\nexport function useProducts(loader?: string) {\n return useQuery({\n queryKey: [endpoint],\n queryFn: () => fetchProducts(),\n initialData: () => {\n if (loader) {\n // @ts-ignore\n return getBootstrapData().loaders?.[loader];\n }\n },\n });\n}\n\nfunction fetchProducts(): Promise {\n return apiClient\n .get>(endpoint)\n .then(response => {\n return {products: response.data.pagination.data};\n });\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const ForumIcon = createSvgIcon(\n \n, 'ForumOutlined');\n","import React, {ComponentPropsWithoutRef, forwardRef} from 'react';\nimport clsx from 'clsx';\nimport {mergeProps, useObjectRef} from '@react-aria/utils';\nimport {useController} from 'react-hook-form';\nimport {AutoFocusProps, useAutoFocus} from '../../focus/use-auto-focus';\n\ntype RadioSize = 'xs' | 'sm' | 'md' | 'lg' | undefined;\n\nexport interface RadioProps\n extends AutoFocusProps,\n Omit, 'size'> {\n size?: RadioSize;\n value: string;\n invalid?: boolean;\n isFirst?: boolean;\n}\nexport const Radio = forwardRef((props, ref) => {\n const {children, autoFocus, size, invalid, isFirst, ...domProps} = props;\n\n const inputRef = useObjectRef(ref);\n useAutoFocus({autoFocus}, inputRef);\n\n const sizeClassNames = getSizeClassNames(size);\n\n return (\n \n \n {children && {children}}\n \n );\n});\n\nexport function FormRadio(props: RadioProps) {\n const {\n field: {onChange, onBlur, value, ref},\n fieldState: {invalid},\n } = useController({\n name: props.name!,\n });\n\n const formProps: Partial = {\n onChange,\n onBlur,\n checked: props.value === value,\n invalid: props.invalid || invalid,\n };\n\n return ;\n}\n\nfunction getSizeClassNames(size?: RadioSize): {\n circle: string;\n label: string;\n} {\n switch (size) {\n case 'xs':\n return {circle: 'h-12 w-12', label: 'text-xs'};\n case 'sm':\n return {circle: 'h-16 w-16', label: 'text-sm'};\n case 'lg':\n return {circle: 'h-24 w-24', label: 'text-lg'};\n default:\n return {circle: 'h-20 w-20', label: 'text-base'};\n }\n}\n","import {\n Children,\n cloneElement,\n forwardRef,\n isValidElement,\n ReactNode,\n useId,\n} from 'react';\nimport clsx from 'clsx';\nimport {useController} from 'react-hook-form';\nimport {Orientation} from '../orientation';\nimport {RadioProps} from './radio';\nimport {getInputFieldClassNames} from '../input-field/get-input-field-class-names';\n\nexport interface RadioGroupProps {\n children: ReactNode;\n orientation?: Orientation;\n size?: 'xs' | 'sm' | 'md' | 'lg';\n className?: string;\n label?: ReactNode;\n disabled?: boolean;\n name?: string;\n errorMessage?: ReactNode;\n description?: ReactNode;\n invalid?: boolean;\n required?: boolean;\n}\nexport const RadioGroup = forwardRef(\n (props, ref) => {\n const style = getInputFieldClassNames(props);\n const {\n label,\n children,\n size,\n className,\n orientation = 'horizontal',\n disabled,\n required,\n invalid,\n errorMessage,\n description,\n } = props;\n\n const labelProps = {};\n const id = useId();\n const name = props.name || id;\n\n return (\n \n {label && (\n \n {label}\n \n )}\n \n {Children.map(children, child => {\n if (isValidElement(child)) {\n return cloneElement(child, {\n name,\n size,\n invalid: child.props.invalid || invalid || undefined,\n disabled: child.props.disabled || disabled,\n required: child.props.required || required,\n });\n }\n })}\n \n {description && !errorMessage && (\n
    \n {description}\n
    \n )}\n {errorMessage &&
    {errorMessage}
    }\n \n );\n }\n);\n\ninterface FormRadioGroupProps extends RadioGroupProps {\n name: string;\n}\nexport function FormRadioGroup({children, ...props}: FormRadioGroupProps) {\n const {\n fieldState: {error},\n } = useController({\n name: props.name!,\n });\n return (\n \n {children}\n \n );\n}\n","import {Price} from '../price';\n\nexport type UpsellBillingCycle = 'monthly' | 'yearly';\n\nexport function findBestPrice(\n token: UpsellBillingCycle,\n prices: Price[]\n): Price | undefined {\n if (token === 'monthly') {\n const match = findMonthlyPrice(prices);\n if (match) return match;\n }\n\n if (token === 'yearly') {\n const match = findYearlyPrice(prices);\n if (match) return match;\n }\n\n return prices[0];\n}\n\nfunction findYearlyPrice(prices: Price[]) {\n return prices.find(price => {\n if (price.interval === 'month' && price.interval_count >= 12) {\n return price;\n }\n if (price.interval === 'year' && price.interval_count >= 1) {\n return price;\n }\n });\n}\n\nfunction findMonthlyPrice(prices: Price[]) {\n return prices.find(price => {\n if (price.interval === 'day' && price.interval_count >= 30) {\n return price;\n }\n if (price.interval === 'month' && price.interval_count >= 1) {\n return price;\n }\n });\n}\n","// find the highest percentage decrease between monthly and yearly prices of specified products\nimport {Product} from '../product';\nimport {findBestPrice} from './find-best-price';\nimport {Fragment, memo} from 'react';\nimport {Trans} from '../../i18n/trans';\n\ninterface UpsellLabelProps {\n products?: Product[];\n}\nexport const UpsellLabel = memo(({products}: UpsellLabelProps) => {\n const upsellPercentage = calcHighestUpsellPercentage(products);\n\n if (upsellPercentage <= 0) {\n return null;\n }\n\n return (\n \n \n {' '}\n (\n \n )\n \n \n );\n});\n\nfunction calcHighestUpsellPercentage(products?: Product[]) {\n if (!products?.length) return 0;\n\n const decreases = products.map(product => {\n const monthly = findBestPrice('monthly', product.prices);\n const yearly = findBestPrice('yearly', product.prices);\n\n if (!monthly || !yearly) return 0;\n\n // monthly plan per year amount\n const monthlyAmount = monthly.amount * 12;\n const yearlyAmount = yearly.amount;\n\n const savingsPercentage = Math.round(\n ((monthlyAmount - yearlyAmount) / monthlyAmount) * 100\n );\n\n if (savingsPercentage > 0 && savingsPercentage <= 200) {\n return savingsPercentage;\n }\n\n return 0;\n });\n\n return Math.max(Math.max(...decreases), 0);\n}\n","import {Radio} from '../../ui/forms/radio-group/radio';\nimport {UpsellBillingCycle} from './find-best-price';\nimport {Trans} from '../../i18n/trans';\nimport {\n RadioGroup,\n RadioGroupProps,\n} from '../../ui/forms/radio-group/radio-group';\nimport {UpsellLabel} from './upsell-label';\nimport {Product} from '../product';\n\ninterface BillingCycleRadioProps extends Omit {\n selectedCycle: UpsellBillingCycle;\n onChange: (value: UpsellBillingCycle) => void;\n products?: Product[];\n}\nexport function BillingCycleRadio({\n selectedCycle,\n onChange,\n products,\n ...radioGroupProps\n}: BillingCycleRadioProps) {\n return (\n \n {\n onChange(e.target.value as UpsellBillingCycle);\n }}\n >\n \n \n
    \n {\n onChange(e.target.value as UpsellBillingCycle);\n }}\n >\n \n \n \n );\n}\n","import {createSvgIcon} from '../../../../icons/create-svg-icon';\n\nexport const CancelFilledIcon = createSvgIcon(\n \n);\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const WarningIcon = createSvgIcon(\n \n, 'WarningOutlined');\n","import {\n cloneElement,\n forwardRef,\n Fragment,\n HTMLAttributes,\n ReactElement,\n Ref,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from 'react';\nimport {AnimatePresence, m} from 'framer-motion';\nimport clsx from 'clsx';\nimport {PopoverAnimation} from '../overlays/popover-animation';\nimport {useFloatingPosition} from '../overlays/floating-position';\nimport {createPortal} from 'react-dom';\nimport {mergeProps} from '@react-aria/utils';\nimport {OffsetOptions, Placement} from '@floating-ui/react-dom';\nimport {rootEl} from '../../core/root-el';\nimport {MessageDescriptor} from '@common/i18n/message-descriptor';\n\nconst TOOLTIP_COOLDOWN = 500;\nconst tooltips: Record void) | undefined> =\n {};\nlet globalWarmedUp = false;\nlet globalWarmUpTimeout: ReturnType | null = null;\nlet globalCooldownTimeout: ReturnType | null = null;\n\nconst closeOpenTooltips = (tooltipId: string) => {\n for (const hideTooltipId in tooltips) {\n if (hideTooltipId !== tooltipId) {\n tooltips[hideTooltipId]?.(true);\n delete tooltips[hideTooltipId];\n }\n }\n};\n\ninterface Props {\n label: ReactElement | string;\n placement?: Placement;\n children: ReactElement;\n variant?: 'neutral' | 'positive' | 'danger';\n delay?: number;\n isDisabled?: boolean;\n offset?: OffsetOptions;\n usePortal?: boolean;\n}\nexport const Tooltip = forwardRef(\n (\n {\n children,\n label,\n placement = 'top',\n offset = 10,\n variant = 'neutral',\n delay = 1500,\n isDisabled,\n usePortal = true,\n ...domProps\n },\n ref\n ) => {\n const {x, y, reference, strategy, arrowRef, arrowStyle, refs} =\n useFloatingPosition({\n placement,\n offset,\n ref,\n showArrow: true,\n });\n\n const [isOpen, setIsOpen] = useState(false);\n const tooltipId = useId();\n const closeTimeout = useRef>();\n\n const showTooltip = () => {\n clearTimeout(closeTimeout.current);\n closeTimeout.current = undefined;\n closeOpenTooltips(tooltipId);\n tooltips[tooltipId] = hideTooltip;\n globalWarmedUp = true;\n setIsOpen(true);\n if (globalWarmUpTimeout) {\n clearTimeout(globalWarmUpTimeout);\n globalWarmUpTimeout = null;\n }\n if (globalCooldownTimeout) {\n clearTimeout(globalCooldownTimeout);\n globalCooldownTimeout = null;\n }\n };\n\n const hideTooltip = useCallback(\n (immediate?: boolean) => {\n if (immediate) {\n clearTimeout(closeTimeout.current);\n closeTimeout.current = undefined;\n setIsOpen(false);\n } else if (!closeTimeout.current) {\n closeTimeout.current = setTimeout(() => {\n closeTimeout.current = undefined;\n setIsOpen(false);\n }, TOOLTIP_COOLDOWN);\n }\n\n if (globalWarmUpTimeout) {\n clearTimeout(globalWarmUpTimeout);\n globalWarmUpTimeout = null;\n }\n if (globalWarmedUp) {\n if (globalCooldownTimeout) {\n clearTimeout(globalCooldownTimeout);\n }\n globalCooldownTimeout = setTimeout(() => {\n delete tooltips[tooltipId];\n globalCooldownTimeout = null;\n globalWarmedUp = false;\n }, TOOLTIP_COOLDOWN);\n }\n },\n [tooltipId]\n );\n\n const warmupTooltip = () => {\n closeOpenTooltips(tooltipId);\n tooltips[tooltipId] = hideTooltip;\n if (!isOpen && !globalWarmUpTimeout && !globalWarmedUp) {\n globalWarmUpTimeout = setTimeout(() => {\n globalWarmUpTimeout = null;\n globalWarmedUp = true;\n showTooltip();\n }, delay);\n } else if (!isOpen) {\n showTooltip();\n }\n };\n\n const showTooltipWithWarmup = (immediate?: boolean) => {\n if (!immediate && delay > 0 && !closeTimeout.current) {\n warmupTooltip();\n } else {\n showTooltip();\n }\n };\n\n // close on unmount\n useEffect(() => {\n return () => {\n clearTimeout(closeTimeout.current);\n const tooltip = tooltips[tooltipId];\n if (tooltip) {\n delete tooltips[tooltipId];\n }\n };\n }, [tooltipId]);\n\n // close on \"escape\" key press\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n hideTooltip(true);\n }\n };\n if (isOpen) {\n document.addEventListener('keydown', onKeyDown, true);\n return () => {\n document.removeEventListener('keydown', onKeyDown, true);\n };\n }\n }, [isOpen, hideTooltip]);\n\n const tooltipContent = (\n \n {isOpen && (\n {\n showTooltipWithWarmup(true);\n }}\n onPointerLeave={() => {\n hideTooltip();\n }}\n className={clsx(\n 'z-tooltip my-4 max-w-240 break-words rounded px-8 py-4 text-xs text-white shadow',\n variant === 'positive' && 'bg-positive',\n variant === 'danger' && 'bg-danger',\n variant === 'neutral' && 'bg-toast'\n )}\n style={{\n position: strategy,\n top: y ?? '',\n left: x ?? '',\n }}\n >\n }\n className=\"absolute h-8 w-8 rotate-45 bg-inherit\"\n style={arrowStyle}\n />\n {label}\n \n )}\n \n );\n\n return (\n \n {cloneElement(\n children,\n // pass dom props down to child element, in case tooltip is wrapped in menu trigger\n mergeProps(\n {\n 'aria-describedby': isOpen ? tooltipId : undefined,\n ref: reference,\n onPointerEnter: e => {\n if (e.pointerType === 'mouse') {\n showTooltipWithWarmup();\n }\n },\n onFocus: e => {\n if (e.target.matches(':focus-visible')) {\n showTooltipWithWarmup(true);\n }\n },\n onPointerLeave: e => {\n if (e.pointerType === 'mouse') {\n hideTooltip();\n }\n },\n onPointerDown: () => {\n hideTooltip(true);\n },\n onBlur: () => {\n hideTooltip();\n },\n 'aria-label':\n typeof label === 'string' ? label : label.props.message,\n } as HTMLAttributes,\n domProps\n )\n )}\n {usePortal\n ? rootEl && createPortal(tooltipContent, rootEl)\n : tooltipContent}\n \n );\n }\n);\n","import React, {\n cloneElement,\n JSXElementConstructor,\n ReactElement,\n ReactNode,\n useRef,\n} from 'react';\nimport clsx from 'clsx';\nimport {useFocusManager} from '@react-aria/focus';\nimport {ButtonBase} from '../../../buttons/button-base';\nimport {CancelFilledIcon} from './cancel-filled-icon';\nimport {WarningIcon} from '@common/icons/material/Warning';\nimport {Tooltip} from '../../../tooltip/tooltip';\nimport {To} from 'react-router-dom';\n\nexport interface ChipProps {\n onRemove?: () => void;\n disabled?: boolean;\n selectable?: boolean;\n invalid?: boolean;\n errorMessage?: ReactElement | string;\n children?: ReactNode;\n className?: string;\n adornment?: null | ReactElement<{\n size: string;\n className?: string;\n circle?: boolean;\n }>;\n radius?: string;\n color?: 'chip' | 'primary' | 'danger' | 'positive';\n size?: 'xs' | 'sm' | 'md' | 'lg';\n elementType?: 'div' | 'a' | JSXElementConstructor;\n to?: To;\n onClick?: (e: React.MouseEvent) => void;\n}\nexport function Chip(props: ChipProps) {\n const {\n onRemove,\n disabled,\n invalid,\n errorMessage,\n children,\n className,\n selectable = false,\n radius = 'rounded-full',\n elementType = 'div',\n to,\n onClick,\n } = props;\n const chipRef = useRef(null);\n const deleteButtonRef = useRef(null);\n const focusManager = useFocusManager();\n\n const handleKeyDown = (e: React.KeyboardEvent) => {\n switch (e.key) {\n case 'ArrowRight':\n case 'ArrowDown':\n focusManager?.focusNext({tabbable: true});\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n focusManager?.focusPrevious({tabbable: true});\n break;\n case 'Backspace':\n case 'Delete':\n if (chipRef.current === document.activeElement) {\n onRemove?.();\n }\n break;\n default:\n }\n };\n\n const handleClick: React.MouseEventHandler = e => {\n e.stopPropagation();\n if (onClick) {\n onClick(e);\n } else {\n chipRef.current!.focus();\n }\n };\n\n const sizeStyle = sizeClassNames(props);\n\n let adornment =\n invalid || errorMessage != null ? (\n \n ) : (\n props.adornment &&\n cloneElement(props.adornment, {\n size: sizeStyle.adornment.size,\n circle: true,\n className: clsx(props.adornment.props, sizeStyle.adornment.margin),\n })\n );\n\n if (errorMessage && adornment) {\n adornment = (\n \n {adornment}\n \n );\n }\n\n const Element = elementType;\n\n return (\n \n {adornment}\n {children}\n {onRemove && (\n {\n e.stopPropagation();\n onRemove();\n }}\n tabIndex={-1}\n >\n \n \n )}\n
    \n );\n}\n\nfunction sizeClassNames({size, onRemove}: ChipProps) {\n switch (size) {\n case 'xs':\n return {\n adornment: {size: 'xs', margin: '-ml-3'},\n chip: clsx('pl-8 h-20 text-xs font-medium w-max', !onRemove && 'pr-8'),\n closeButton: 'mr-4 w-14 h-14',\n };\n case 'sm':\n return {\n adornment: {size: 'xs', margin: '-ml-3'},\n chip: clsx('pl-8 h-26 text-xs', !onRemove && 'pr-8'),\n closeButton: 'mr-4 w-18 h-18',\n };\n case 'lg':\n return {\n adornment: {size: 'md', margin: '-ml-12'},\n chip: clsx('pl-18 h-38 text-base', !onRemove && 'pr-18'),\n closeButton: 'mr-6 w-24 h-24',\n };\n default:\n return {\n adornment: {size: 'sm', margin: '-ml-6'},\n chip: clsx('pl-12 h-32 text-sm', !onRemove && 'pr-12'),\n closeButton: 'mr-6 w-22 h-22',\n };\n }\n}\n\nfunction colorClassName({color}: ChipProps): string {\n switch (color) {\n case 'primary':\n return `bg-primary text-on-primary`;\n case 'positive':\n return `bg-positive-lighter text-positive-darker`;\n case 'danger':\n return `bg-danger-lighter text-danger-darker`;\n default:\n return `bg-chip text-main`;\n }\n}\n","import {Fragment, memo} from 'react';\nimport {useNumberFormatter} from './use-number-formatter';\n\ninterface FormattedCurrencyProps {\n value: number;\n currency: string;\n}\nexport const FormattedCurrency = memo(\n ({value, currency}: FormattedCurrencyProps) => {\n const formatter = useNumberFormatter({\n style: 'currency',\n currency,\n currencyDisplay: 'narrowSymbol',\n });\n\n if (isNaN(value)) {\n value = 0;\n }\n\n return {formatter.format(value)};\n }\n);\n","import {FormattedCurrency} from './formatted-currency';\nimport React from 'react';\nimport {Price} from '../billing/price';\nimport {Trans} from './trans';\nimport clsx from 'clsx';\n\ninterface FormattedPriceProps {\n price?: Omit;\n variant?: 'slash' | 'separateLine';\n className?: string;\n priceClassName?: string;\n periodClassName?: string;\n}\nexport function FormattedPrice({\n price,\n variant = 'slash',\n className,\n priceClassName,\n periodClassName,\n}: FormattedPriceProps) {\n if (!price) return null;\n\n const translatedInterval = ;\n\n return (\n
    \n
    \n \n
    \n {variant === 'slash' ? (\n
    / {translatedInterval}
    \n ) : (\n
    \n
    {translatedInterval}\n
    \n )}\n
    \n );\n}\n","import {Trans} from '../../i18n/trans';\nimport {CheckIcon} from '../../icons/material/Check';\nimport {Product} from '../product';\n\ninterface FeatureListProps {\n product: Product;\n}\n\nexport function ProductFeatureList({product}: FeatureListProps) {\n if (!product.feature_list.length) return null;\n\n return (\n
    \n
    \n \n
    \n {product.feature_list.map(feature => (\n
    \n \n \n
    \n ))}\n
    \n );\n}\n","import {AnimatePresence, m} from 'framer-motion';\nimport {Fragment} from 'react';\nimport {opacityAnimation} from '@common/ui/animation/opacity-animation';\nimport {Skeleton} from '@common/ui/skeleton/skeleton';\nimport {useProducts} from '@common/billing/pricing-table/use-products';\nimport {Product} from '@common/billing/product';\nimport {\n findBestPrice,\n UpsellBillingCycle,\n} from '@common/billing/pricing-table/find-best-price';\nimport {useAuth} from '@common/auth/use-auth';\nimport clsx from 'clsx';\nimport {Chip} from '@common/ui/forms/input-field/chip-field/chip';\nimport {Trans} from '@common/i18n/trans';\nimport {FormattedPrice} from '@common/i18n/formatted-price';\nimport {Button} from '@common/ui/buttons/button';\nimport {Link} from 'react-router-dom';\nimport {setInLocalStorage} from '@common/utils/hooks/local-storage';\nimport {ProductFeatureList} from '@common/billing/pricing-table/product-feature-list';\n\ninterface PricingTableProps {\n selectedCycle: UpsellBillingCycle;\n className?: string;\n productLoader?: string;\n}\nexport function PricingTable({\n selectedCycle,\n className,\n productLoader,\n}: PricingTableProps) {\n const query = useProducts(productLoader);\n return (\n \n \n {query.data ? (\n \n ) : (\n \n )}\n \n \n );\n}\n\ninterface PlanListProps {\n plans: Product[];\n selectedPeriod: UpsellBillingCycle;\n}\nfunction PlanList({plans, selectedPeriod}: PlanListProps) {\n const {isLoggedIn, isSubscribed} = useAuth();\n const filteredPlans = plans.filter(plan => !plan.hidden);\n return (\n \n {filteredPlans.map((plan, index) => {\n const isFirst = index === 0;\n const isLast = index === filteredPlans.length - 1;\n const price = findBestPrice(selectedPeriod, plan.prices);\n\n let upgradeRoute;\n if (!isLoggedIn) {\n upgradeRoute = `/register?redirectFrom=pricing`;\n }\n if (isSubscribed) {\n upgradeRoute = `/change-plan/${plan.id}/${price?.id}/confirm`;\n }\n if (isLoggedIn && !plan.free) {\n upgradeRoute = `/checkout/${plan.id}/${price?.id}`;\n }\n\n return (\n \n
    \n \n \n \n
    \n \n
    \n
    \n \n
    \n
    \n
    \n {price ? (\n \n ) : (\n
    \n \n
    \n )}\n
    \n {\n if (isLoggedIn || !price || !plan) return;\n setInLocalStorage('be.onboarding.selected', {\n productId: plan.id,\n priceId: price.id,\n });\n }}\n to={upgradeRoute}\n >\n \n \n
    \n \n
    \n \n );\n })}\n
    \n );\n}\n\ninterface ActionButtonTextProps {\n product: Product;\n}\nfunction ActionButtonText({product}: ActionButtonTextProps) {\n const {isLoggedIn} = useAuth();\n if (product.free && isLoggedIn) {\n return ;\n }\n if (product.free || !isLoggedIn) {\n return ;\n }\n return ;\n}\n\nfunction SkeletonLoader() {\n return (\n \n \n \n \n \n );\n}\n\nfunction PlanSkeleton() {\n return (\n \n \n \n \n \n \n \n \n \n );\n}\n","import {useProducts} from './use-products';\nimport {Button} from '../../ui/buttons/button';\nimport {Trans} from '../../i18n/trans';\nimport {ForumIcon} from '../../icons/material/Forum';\nimport {Navbar} from '../../ui/navigation/navbar/navbar';\nimport {Link} from 'react-router-dom';\nimport {Footer} from '../../ui/footer/footer';\nimport {Fragment, useState} from 'react';\nimport {UpsellBillingCycle} from './find-best-price';\nimport {BillingCycleRadio} from './billing-cycle-radio';\nimport {StaticPageTitle} from '../../seo/static-page-title';\nimport {PricingTable} from '@common/billing/pricing-table/pricing-table';\n\nexport function PricingPage() {\n const query = useProducts('pricingPage');\n const [selectedCycle, setSelectedCycle] =\n useState('yearly');\n\n return (\n \n \n \n \n \n
    \n

    \n \n

    \n\n \n\n \n \n
    \n