{"version":3,"file":"admin-routes-06d8abf9.mjs","sources":["../../../common/resources/client/admin/admin-sidebar.tsx","../../../common/resources/client/ui/layout/dashboard-layout-context.ts","../../../common/resources/client/utils/hooks/use-block-body-overflow.ts","../../../common/resources/client/ui/layout/dashboard-layout.tsx","../../../common/resources/client/ui/layout/dashboard-content.tsx","../../../common/resources/client/ui/layout/dashboard-sidenav.tsx","../../../common/resources/client/icons/material/MenuOpen.tsx","../../../common/resources/client/ui/layout/dashboard-navbar.tsx","../../../common/resources/client/admin/use-admin-setup-alerts.ts","../../../common/resources/client/admin/admin-layout.tsx","../../../common/resources/client/datatable/filters/timestamp-filters.ts","../../../common/resources/client/admin/users/user-datatable-filters.ts","../../../common/resources/client/datatable/page/data-table-context.ts","../../../common/resources/client/datatable/data-table-pagination-footer.tsx","../../../common/resources/client/datatable/data-table-header.tsx","../../../common/resources/client/datatable/selected-state-datatable-header.tsx","../../../common/resources/client/datatable/data-table.tsx","../../../common/resources/client/datatable/page/data-table-page.tsx","../../../common/resources/client/datatable/requests/delete-selected-rows.ts","../../../common/resources/client/datatable/page/delete-selected-items-action.tsx","../../../common/resources/client/datatable/page/data-table-emty-state-message.tsx","../../../common/resources/client/admin/roles/team.svg","../../../common/resources/client/datatable/data-table-add-item-button.tsx","../../../common/resources/client/icons/material/FileDownload.tsx","../../../common/resources/client/datatable/requests/use-export-csv.ts","../../../common/resources/client/uploads/utils/download-file-from-url.ts","../../../common/resources/client/datatable/csv-export/csv-export-info-dialog.tsx","../../../common/resources/client/datatable/csv-export/data-table-export-csv-button.tsx","../../../common/resources/client/icons/material/PersonOff.tsx","../../../common/resources/client/admin/users/requests/use-ban-user.ts","../../../common/resources/client/ui/forms/input-field/date/date-picker/use-date-picker-state.ts","../../../common/resources/client/ui/forms/input-field/date/date-picker/date-picker.tsx","../../../common/resources/client/admin/users/ban-user-dialog.tsx","../../../common/resources/client/admin/users/requests/use-unban-user.ts","../../../common/resources/client/admin/users/requests/use-impersonate-user.ts","../../../common/resources/client/admin/users/user-datatable-columns.tsx","../../../common/resources/client/admin/users/user-datatable.tsx","../../../common/resources/client/utils/array/chunk-array.ts","../../../common/resources/client/admin/appearance/config/default-appearance-config.ts","../../../common/resources/client/admin/appearance/appearance-button.tsx","../../../common/resources/client/admin/appearance/sections/themes/color-icon.tsx","../../../common/resources/client/ui/color-picker/color-swatch.tsx","../../../common/resources/client/ui/color-picker/color-presets.ts","../../../common/resources/client/ui/color-picker/color-picker.tsx","../../../common/resources/client/ui/color-picker/color-picker-dialog.tsx","../../../resources/client/admin/appearance/sections/landing-page-section/landing-page-section-general.tsx","../../../common/resources/client/utils/string/uc-first.ts","../../../common/resources/client/auth/ui/permission-selector.tsx","../../../common/resources/client/admin/appearance/sections/menus/hooks/available-routes.ts","../../../common/resources/client/ui/icon-picker/icon-grid-style.ts","../../../common/resources/client/ui/icon-picker/icon-picker.tsx","../../../common/resources/client/ui/icon-picker/icon-picker-dialog.tsx","../../../common/resources/client/admin/menus/menu-item-form.tsx","../../../resources/client/admin/appearance/sections/landing-page-section/landing-page-section-action-buttons.tsx","../../../resources/client/admin/appearance/sections/landing-page-section/landing-page-section-primary-features.tsx","../../../resources/client/admin/appearance/sections/landing-page-section/landing-page-section-secondary-features.tsx","../../../resources/client/admin/appearance/app-appearance-config.tsx","../../../common/resources/client/admin/appearance/config/merged-appearance-config.ts","../../../common/resources/client/admin/appearance/appearance-store.ts","../../../common/resources/client/admin/appearance/requests/save-appearance-changes.ts","../../../common/resources/client/admin/appearance/requests/appearance-values.ts","../../../common/resources/client/admin/appearance/section-header.tsx","../../../common/resources/client/admin/appearance/appearance-layout.tsx","../../../common/resources/client/admin/appearance/sections/menus/menu-list.tsx","../../../common/resources/client/admin/appearance/sections/menus/add-menu-item-dialog.tsx","../../../common/resources/client/icons/material/DragIndicator.tsx","../../../common/resources/client/icons/material/Delete.tsx","../../../common/resources/client/admin/appearance/sections/menus/dropdown-menu.svg","../../../common/resources/client/admin/appearance/sections/menus/menu-editor.tsx","../../../common/resources/client/admin/appearance/sections/menus/menu-item-editor.tsx","../../../common/resources/client/admin/appearance/sections/general-section.tsx","../../../common/resources/client/utils/string/random-number.ts","../../../common/resources/client/admin/appearance/sections/themes/theme-list.tsx","../../../common/resources/client/ace-editor/ace-dialog.tsx","../../../common/resources/client/admin/appearance/sections/seo/use-seo-tags.ts","../../../common/resources/client/admin/appearance/sections/seo/use-update-seo-tags.ts","../../../common/resources/client/admin/appearance/sections/seo/seo-section.tsx","../../../common/resources/client/admin/appearance/sections/code/custom-code-section.tsx","../../../common/resources/client/admin/custom-pages/articles.svg","../../../common/resources/client/auth/user.ts","../../../common/resources/client/admin/custom-pages/custom-page-datatable-filters.tsx","../../../common/resources/client/admin/custom-pages/custom-page-datatable-columns.tsx","../../../common/resources/client/admin/custom-pages/custom-page-datable-page.tsx","../../../resources/client/admin/settings/app-settings-nav-config.ts","../../../common/resources/client/admin/settings/settings-nav-config.ts","../../../common/resources/client/admin/settings/settings-layout.tsx","../../../common/resources/client/admin/settings/requests/use-admin-settings.ts","../../../common/resources/client/admin/settings/generate-sitemap.ts","../../../common/resources/client/admin/settings/requests/update-admin-settings.ts","../../../common/resources/client/admin/settings/settings-panel.tsx","../../../common/resources/client/admin/settings/settings-separator.tsx","../../../common/resources/client/icons/material/Link.tsx","../../../common/resources/client/admin/settings/learn-more-link.tsx","../../../common/resources/client/admin/settings/pages/general-settings.tsx","../../../common/resources/client/ui/themes/utils/color-to-theme-value.ts","../../../common/resources/client/admin/appearance/sections/themes/theme-settings-dialog-trigger.tsx","../../../common/resources/client/icons/material/RestartAlt.tsx","../../../common/resources/client/admin/appearance/sections/themes/theme-more-options-button.tsx","../../../common/resources/client/admin/appearance/sections/themes/navbar-color-picker.tsx","../../../common/resources/client/ui/themes/utils/theme-value-to-hex.ts","../../../common/resources/client/admin/appearance/sections/themes/theme-editor.tsx","../../../common/resources/client/admin/settings/json-chip-field.tsx","../../../resources/client/admin/settings/video-settings.tsx","../../../common/resources/client/ui/tabs/tab-panels.tsx","../../../resources/client/admin/settings/content-settings/content-settings-general-panel.tsx","../../../common/resources/client/admin/settings/settings-error-group.tsx","../../../resources/client/admin/settings/content-settings/content-settings-automation-panel.tsx","../../../resources/client/admin/settings/content-settings/content-settings-title-page-panel.tsx","../../../resources/client/admin/settings/content-settings/content-settings.tsx","../../../common/resources/client/admin/settings/pages/search-settings/requests/use-search-models.ts","../../../common/resources/client/admin/settings/pages/search-settings/requests/use-import-search-models.ts","../../../common/resources/client/admin/settings/pages/search-settings/search-settings.tsx","../../../resources/client/admin/settings/app-settings-routes.tsx","../../../common/resources/client/admin/settings/pages/subscription-settings.tsx","../../../common/resources/client/admin/settings/pages/localization-settings.tsx","../../../common/resources/client/admin/settings/pages/authentication-settings.tsx","../../../common/resources/client/admin/settings/pages/uploading-settings/max-server-upload-size.ts","../../../common/resources/client/uploads/utils/space-units.ts","../../../common/resources/client/uploads/utils/convert-to-bytes.ts","../../../common/resources/client/ui/forms/input-field/file-size-field.tsx","../../../common/resources/client/admin/settings/pages/uploading-settings/use-upload-s3-cors.ts","../../../common/resources/client/admin/settings/pages/uploading-settings/dropbox-form/use-generate-dropbox-refresh-token.ts","../../../common/resources/client/admin/settings/pages/uploading-settings/dropbox-form/dropbox-form.tsx","../../../common/resources/client/admin/settings/pages/uploading-settings/uploading-settings.tsx","../../../common/resources/client/admin/settings/pages/mail-settings/mailgun-credentials.tsx","../../../common/resources/client/admin/settings/pages/mail-settings/smtp-credentials.tsx","../../../common/resources/client/admin/settings/pages/mail-settings/ses-credentials.tsx","../../../common/resources/client/admin/settings/pages/mail-settings/postmark-credentials.tsx","../../../common/resources/client/admin/settings/pages/mail-settings/gmail-icon.tsx","../../../common/resources/client/admin/settings/pages/mail-settings/connect-gmail-panel.tsx","../../../common/resources/client/admin/settings/pages/mail-settings/outgoing-mail-group.tsx","../../../common/resources/client/admin/settings/pages/mail-settings/outgoing-email-settings.tsx","../../../common/resources/client/admin/settings/pages/cache-settings/clear-cache.ts","../../../common/resources/client/admin/settings/pages/cache-settings/cache-settings.tsx","../../../common/resources/client/admin/settings/pages/logging-settings.tsx","../../../common/resources/client/admin/settings/pages/queue-settings.tsx","../../../common/resources/client/admin/settings/pages/recaptcha-settings.tsx","../../../common/resources/client/ui/forms/input-field/file-field.tsx","../../../common/resources/client/admin/settings/pages/reports-settings.tsx","../../../common/resources/client/admin/users/requests/update-user.ts","../../../common/resources/client/admin/users/crupdate-user-form.tsx","../../../common/resources/client/icons/material/Report.tsx","../../../common/resources/client/admin/users/update-user-page.tsx","../../../common/resources/client/admin/users/requests/create-user.ts","../../../common/resources/client/admin/users/create-user-page.tsx","../../../common/resources/client/icons/material/Translate.tsx","../../../common/resources/client/admin/translations/use-locale-with-lines.ts","../../../common/resources/client/admin/translations/update-localization.ts","../../../common/resources/client/admin/translations/update-localization-dialog.tsx","../../../common/resources/client/admin/translations/create-localization.ts","../../../common/resources/client/admin/translations/create-localization-dialog.tsx","../../../common/resources/client/admin/translations/around-the-world.svg","../../../common/resources/client/admin/translations/use-upload-translation-file.ts","../../../common/resources/client/admin/translations/localization-index.tsx","../../../common/resources/client/admin/translations/new-translation-dialog.tsx","../../../common/resources/client/admin/translations/translation-management-page.tsx","../../../common/resources/client/admin/ads/ads-page.tsx","../../../common/resources/client/admin/appearance/section-list.tsx","../../../common/resources/client/admin/roles/role-index-page-filters.ts","../../../common/resources/client/admin/roles/roles-index-page.tsx","../../../common/resources/client/admin/roles/requests/use-role.ts","../../../common/resources/client/admin/roles/requests/use-update-role.ts","../../../common/resources/client/admin/roles/crupdate-role-page/crupdate-role-settings-panel.tsx","../../../common/resources/client/users/select-user-dialog.tsx","../../../common/resources/client/admin/roles/requests/use-remove-users-from-role.ts","../../../common/resources/client/admin/roles/requests/use-add-users-to-role.ts","../../../common/resources/client/admin/roles/crupdate-role-page/edit-role-page-users-panel.tsx","../../../common/resources/client/admin/roles/crupdate-role-page/edit-role-page.tsx","../../../common/resources/client/admin/roles/requests/user-create-role.ts","../../../common/resources/client/admin/roles/crupdate-role-page/create-role-page.tsx","../../../common/resources/client/admin/tags/tag-index-page-filters.ts","../../../common/resources/client/admin/tags/software-engineer.svg","../../../common/resources/client/admin/tags/crupdate-tag-form.tsx","../../../common/resources/client/admin/tags/requests/use-create-new-tag.ts","../../../common/resources/client/admin/tags/create-tag-dialog.tsx","../../../common/resources/client/admin/tags/requests/use-update-tag.ts","../../../common/resources/client/admin/tags/update-tag-dialog.tsx","../../../common/resources/client/admin/tags/tag-index-page.tsx","../../../common/resources/client/uploads/formatted-bytes.tsx","../../../common/resources/client/icons/material/Visibility.tsx","../../../common/resources/client/admin/file-entry/upload.svg","../../../common/resources/client/uploads/hooks/file-entry-urls.ts","../../../common/resources/client/uploads/preview/file-preview-context.ts","../../../common/resources/client/uploads/preview/file-preview/default-file-preview.tsx","../../../common/resources/client/uploads/preview/file-preview/image-file-preview.tsx","../../../common/resources/client/uploads/preview/file-preview/text-file-preview.tsx","../../../common/resources/client/uploads/preview/file-preview/video-file-preview.tsx","../../../common/resources/client/uploads/preview/file-preview/audio-file-preview.tsx","../../../common/resources/client/uploads/preview/file-preview/pdf-file-preview.tsx","../../../common/resources/client/uploads/preview/file-preview/word-document-file-preview.tsx","../../../common/resources/client/uploads/preview/available-previews.ts","../../../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","../../../common/resources/client/uploads/file-type-icon/file-thumbnail.tsx","../../../common/resources/client/uploads/preview/file-preview-container.tsx","../../../common/resources/client/uploads/preview/file-preview-dialog.tsx","../../../common/resources/client/admin/file-entry/file-entry-index-filters.ts","../../../common/resources/client/admin/file-entry/file-entry-index-page.tsx","../../../common/resources/client/admin/subscriptions/subscription-index-page-filters.ts","../../../common/resources/client/admin/subscriptions/subscriptions.svg","../../../common/resources/client/admin/subscriptions/requests/use-update-subscription.ts","../../../common/resources/client/admin/subscriptions/crupdate-subscription-form.tsx","../../../common/resources/client/admin/subscriptions/update-subscription-dialog.tsx","../../../common/resources/client/admin/subscriptions/requests/use-create-subscription.ts","../../../common/resources/client/admin/subscriptions/create-subscription-dialog.tsx","../../../common/resources/client/icons/material/Pause.tsx","../../../common/resources/client/icons/material/PlayArrow.tsx","../../../common/resources/client/admin/subscriptions/subscriptions-index-page.tsx","../../../common/resources/client/icons/material/Sync.tsx","../../../common/resources/client/admin/plans/requests/use-sync-products.ts","../../../common/resources/client/admin/plans/requests/use-delete-product.ts","../../../common/resources/client/admin/plans/plans-index-page-filters.ts","../../../common/resources/client/admin/plans/plans-index-page.tsx","../../../common/resources/client/admin/plans/requests/use-product.ts","../../../common/resources/client/admin/plans/crupdate-plan-page/billing-period-presets.ts","../../../common/resources/client/admin/plans/crupdate-plan-page/price-form.tsx","../../../common/resources/client/admin/plans/crupdate-plan-page/crupdate-plan-form.tsx","../../../common/resources/client/admin/plans/requests/use-update-product.ts","../../../common/resources/client/admin/plans/crupdate-plan-page/edit-plan-page.tsx","../../../common/resources/client/admin/plans/requests/use-create-product.ts","../../../common/resources/client/admin/plans/crupdate-plan-page/create-plan-page.tsx","../../../common/resources/client/admin/settings/pages/gdpr-settings.tsx","../../../common/resources/client/ui/overlays/dialog/info-dialog-trigger/info-dialog-trigger-icon.tsx","../../../common/resources/client/ui/overlays/dialog/info-dialog-trigger/info-dialog-trigger.tsx","../../../common/resources/client/icons/material/Home.tsx","../../../common/resources/client/admin/channels/channels-datatable-columns.tsx","../../../common/resources/client/admin/channels/requests/use-apply-channel-preset.ts","../../../common/resources/client/admin/channels/channels-docs-link.tsx","../../../common/resources/client/admin/channels/channels-datatable-page.tsx","../../../common/resources/client/admin/channels/requests/use-update-channel.ts","../../../common/resources/client/admin/channels/channel-editor/edit-channel-page-layout.tsx","../../../common/resources/client/icons/material/Description.tsx","../../../common/resources/client/ui/slug-editor.tsx","../../../common/resources/client/admin/channels/channel-editor/controls/channel-name-field.tsx","../../../common/resources/client/admin/channels/channel-editor/controls/content-type-field.tsx","../../../common/resources/client/admin/channels/channel-editor/controls/content-auto-update-field.tsx","../../../resources/client/admin/channels/channel-auto-update-field.tsx","../../../resources/client/titles/models/keyword.ts","../../../resources/client/admin/channels/channel-restriction-field.tsx","../../../common/resources/client/icons/material/Dashboard.tsx","../../../common/resources/client/admin/channels/channel-editor/controls/content-layout-fields.tsx","../../../common/resources/client/admin/channels/channel-editor/controls/channel-pagination-type-field.tsx","../../../common/resources/client/icons/material/Public.tsx","../../../resources/client/admin/channels/channel-seo-fields.tsx","../../../resources/client/admin/channels/edit-channel-page.tsx","../../../common/resources/client/admin/channels/requests/use-create-channel.ts","../../../common/resources/client/admin/channels/channel-editor/create-channel-page-layout.tsx","../../../resources/client/admin/channels/create-channel-page.tsx","../../../resources/client/admin/news/news-datatable-filters.ts","../../../resources/client/admin/news/online-articles.svg","../../../resources/client/admin/news/requests/use-delete-news-article.ts","../../../resources/client/admin/news/news-datatable-columns.tsx","../../../common/resources/client/icons/material/Publish.tsx","../../../resources/client/admin/news/requests/use-import-news-articles.ts","../../../resources/client/admin/news/news-datatable-page.tsx","../../../common/resources/client/comments/comments-datatable-page/delete-comments-button.tsx","../../../common/resources/client/comments/requests/use-update-comment.ts","../../../common/resources/client/comments/requests/use-restore-comments.ts","../../../common/resources/client/comments/comments-datatable-page/restore-comments-button.tsx","../../../common/resources/client/comments/comments-datatable-page/comment-datatable-item.tsx","../../../common/resources/client/comments/comments-datatable-page/public-discussion.svg","../../../common/resources/client/comments/comments-datatable-page/comments-datatable-filters.ts","../../../common/resources/client/comments/comments-datatable-page/comments-datatable-page.tsx","../../../resources/client/admin/reviews/reviews.svg","../../../resources/client/admin/reviews/delete-reviews-button.tsx","../../../resources/client/admin/reviews/requests/use-update-review.ts","../../../resources/client/admin/reviews/review-datatable-item.tsx","../../../resources/client/admin/reviews/reviews-datatable-filters.tsx","../../../resources/client/admin/reviews/reviews-datatable-page.tsx","../../../resources/client/admin/videos/video-files.svg","../../../common/resources/client/datatable/column-templates/boolean-indicator.tsx","../../../common/resources/client/icons/material/BarChart.tsx","../../../resources/client/admin/videos/videos-datatable-columns.tsx","../../../resources/client/admin/reviews/title-filter/title-filter-control.tsx","../../../resources/client/titles/requests/use-titles-autocomplete.ts","../../../resources/client/titles/title-select.tsx","../../../resources/client/admin/reviews/title-filter/title-filter-panel.tsx","../../../resources/client/admin/videos/videos-datatable-filters.tsx","../../../resources/client/admin/videos/videos-datatable-page.tsx","../../../resources/client/admin/videos/requests/use-create-video.ts","../../../common/resources/client/uploads/requests/use-file-entry-model.ts","../../../common/resources/client/ui/forms/input-field/file-entry-field.tsx","../../../resources/client/admin/videos/crupdate/crupdate-caption-dialog.tsx","../../../common/resources/client/icons/material/Subtitles.tsx","../../../resources/client/admin/videos/captions/captions-panel.tsx","../../../resources/client/admin/videos/crupdate/crupdate-video-form.tsx","../../../resources/client/admin/videos/crupdate/create-video-page.tsx","../../../resources/client/admin/videos/requests/use-update-video.ts","../../../resources/client/admin/videos/requests/use-video.ts","../../../resources/client/admin/videos/crupdate/edit-video-page.tsx","../../../resources/client/admin/titles/movie-night.svg","../../../resources/client/admin/titles/titles-datatable-columns.tsx","../../../resources/client/admin/titles/titles-datatable-filters.tsx","../../../resources/client/admin/titles/requests/use-import-single-from-tmdb.ts","../../../resources/client/admin/titles/import/import-single-from-tmdb-dialog.tsx","../../../resources/client/admin/titles/requests/use-import-multiple-from-tmdb.ts","../../../resources/client/admin/titles/import/import-multiple-from-tmdb-dialog.tsx","../../../resources/client/admin/titles/titles-datatable-page.tsx","../../../resources/client/admin/titles/title-editor/edit-title-page.tsx","../../../resources/client/episodes/requests/use-delete-episode.ts","../../../resources/client/admin/titles/title-editor/title-editor-layout.tsx","../../../resources/client/admin/titles/title-editor/seasons-editor/season-editor-layout.tsx","../../../resources/client/admin/titles/title-editor/title-editor-page-status.tsx","../../../resources/client/admin/titles/title-editor/seasons-editor/season-editor-episode-list.tsx","../../../resources/client/admin/titles/requests/use-delete-season.ts","../../../resources/client/admin/titles/requests/use-create-season.ts","../../../resources/client/admin/titles/title-editor/seasons-editor/title-seasons-editor.tsx","../../../resources/client/admin/titles/requests/use-create-title.ts","../../../resources/client/admin/titles/requests/use-update-title.ts","../../../common/resources/client/ui/forms/combobox/form-combobox.tsx","../../../resources/client/admin/titles/title-editor/title-primary-facts-form.tsx","../../../resources/client/admin/titles/title-editor/title-reviews-editor.tsx","../../../common/resources/client/icons/material/ZoomOutMap.tsx","../../../resources/client/admin/titles/requests/use-delete-image.ts","../../../resources/client/admin/titles/requests/use-upload-image.ts","../../../resources/client/admin/titles/title-editor/title-images-editor.tsx","../../../resources/client/admin/titles/title-editor/videos-editor/title-videos-sort-button.tsx","../../../resources/client/admin/videos/requests/use-delete-videos.ts","../../../resources/client/seasons/requests/use-season-episode-numbers.ts","../../../resources/client/admin/titles/title-editor/videos-editor/videos-editor-season-select.tsx","../../../resources/client/admin/titles/title-editor/videos-editor/title-videos-editor.tsx","../../../resources/client/episodes/requests/use-update-episode.ts","../../../resources/client/admin/titles/title-editor/episode-editor/episode-editor-layout.tsx","../../../resources/client/episodes/requests/use-create-episode.ts","../../../resources/client/admin/titles/title-editor/episode-editor/episode-primary-facts-form.tsx","../../../resources/client/admin/titles/requests/use-title-credits.ts","../../../resources/client/admin/titles/requests/use-sort-title-credits.ts","../../../resources/client/admin/titles/requests/use-update-title-credit.ts","../../../resources/client/admin/titles/requests/use-create-title-credit.ts","../../../resources/client/admin/titles/title-editor/credits-editor/add-credit-dialog.tsx","../../../resources/client/admin/titles/title-editor/credits-editor/edit-credit-dialog.tsx","../../../resources/client/admin/titles/requests/use-delete-title-credit.ts","../../../resources/client/admin/titles/title-editor/credits-editor/get-credits-editor-action-column.tsx","../../../common/resources/client/icons/material/RecentActors.tsx","../../../resources/client/admin/titles/title-editor/credits-editor/credits-table-query-indicator.tsx","../../../resources/client/admin/titles/title-editor/credits-editor/cast-editor-table.tsx","../../../resources/client/admin/titles/title-editor/credits-editor/title-credits-table-header.tsx","../../../resources/client/admin/titles/title-editor/episode-editor/episode-cast-editor.tsx","../../../resources/client/admin/titles/title-editor/credits-editor/title-cast-editor.tsx","../../../resources/client/admin/titles/title-editor/credits-editor/crew-editor-table.tsx","../../../resources/client/admin/titles/title-editor/credits-editor/title-crew-editor.tsx","../../../resources/client/admin/titles/title-editor/seasons-editor/season-cast-editor.tsx","../../../resources/client/admin/titles/title-editor/seasons-editor/season-crew-editor.tsx","../../../resources/client/admin/titles/title-editor/episode-editor/episode-crew-editor.tsx","../../../resources/client/admin/titles/requests/use-detach-title-tag.ts","../../../resources/client/admin/titles/requests/use-attach-title-tag.ts","../../../resources/client/admin/titles/title-editor/title-tags-editor/add-title-tag-dialog.tsx","../../../resources/client/admin/titles/title-editor/title-tags-editor/title-tags-editor.tsx","../../../resources/client/admin/titles/title-editor/title-comments-editor.tsx","../../../resources/client/admin/people/awards.svg","../../../resources/client/admin/people/people-datatable-columns.tsx","../../../resources/client/admin/people/people-datatable-filters.tsx","../../../resources/client/admin/people/people-datatable-page.tsx","../../../resources/client/admin/people/requests/use-create-person.ts","../../../resources/client/admin/people/crupdate/person-primary-facts-form.tsx","../../../resources/client/admin/people/crupdate/create-person-page.tsx","../../../resources/client/admin/people/requests/use-update-person.ts","../../../resources/client/admin/people/crupdate/update-person-page.tsx","../../../resources/client/admin/people/requests/use-delete-person-credit.ts","../../../resources/client/admin/people/crupdate/person-credits-editor.tsx","../../../common/resources/client/article-editor/article-editor-title.tsx","../../../common/resources/client/icons/material/Undo.tsx","../../../common/resources/client/icons/material/Redo.tsx","../../../common/resources/client/text-editor/menubar/history-buttons.tsx","../../../common/resources/client/icons/material/Code.tsx","../../../common/resources/client/text-editor/menubar/mode-button.tsx","../../../common/resources/client/text-editor/menubar/divider.tsx","../../../common/resources/client/icons/material/FormatBold.tsx","../../../common/resources/client/icons/material/FormatItalic.tsx","../../../common/resources/client/icons/material/FormatUnderlined.tsx","../../../common/resources/client/text-editor/menubar/font-style-buttons.tsx","../../../common/resources/client/icons/material/FormatListBulleted.tsx","../../../common/resources/client/icons/material/FormatListNumbered.tsx","../../../common/resources/client/text-editor/menubar/list-buttons.tsx","../../../common/resources/client/text-editor/insert-link-into-text-editor.ts","../../../common/resources/client/text-editor/menubar/link-button.tsx","../../../common/resources/client/text-editor/menubar/image-button.tsx","../../../common/resources/client/icons/material/FormatClear.tsx","../../../common/resources/client/text-editor/menubar/clear-format-button.tsx","../../../common/resources/client/icons/material/HorizontalRule.tsx","../../../common/resources/client/icons/material/PriorityHigh.tsx","../../../common/resources/client/icons/material/Note.tsx","../../../common/resources/client/icons/material/SmartDisplay.tsx","../../../common/resources/client/text-editor/menubar/insert-menu-trigger.tsx","../../../common/resources/client/ui/keyboard/keyboard.tsx","../../../common/resources/client/text-editor/menubar/format-menu-trigger.tsx","../../../common/resources/client/icons/material/FormatColorText.tsx","../../../common/resources/client/icons/material/FormatColorFill.tsx","../../../common/resources/client/text-editor/menubar/color-buttons.tsx","../../../common/resources/client/icons/material/FormatAlignLeft.tsx","../../../common/resources/client/icons/material/FormatAlignCenter.tsx","../../../common/resources/client/icons/material/FormatAlignRight.tsx","../../../common/resources/client/icons/material/FormatAlignJustify.tsx","../../../common/resources/client/text-editor/menubar/align-buttons.tsx","../../../common/resources/client/icons/material/FormatIndentDecrease.tsx","../../../common/resources/client/icons/material/FormatIndentIncrease.tsx","../../../common/resources/client/text-editor/menubar/indent-buttons.tsx","../../../common/resources/client/text-editor/menubar/code-block-menu-trigger.tsx","../../../common/resources/client/article-editor/article-body-editor-menubar.tsx","../../../common/resources/client/article-editor/article-editor-sticky-header.tsx","../../../resources/client/admin/news/requests/use-update-news-article.ts","../../../resources/client/admin/news/edit-news-article-page.tsx","../../../resources/client/admin/news/requests/use-create-news-article.ts","../../../resources/client/admin/news/create-news-article-page.tsx","../../../resources/client/admin/title-tags/title-tags-editor/requests/use-create-title-tag.ts","../../../resources/client/admin/title-tags/title-tags-editor/create-title-tag-dialog.tsx","../../../resources/client/admin/title-tags/title-tags-editor/requests/use-update-title-tag.ts","../../../resources/client/admin/title-tags/title-tags-editor/update-title-tag-dialog.tsx","../../../resources/client/admin/title-tags/title-tags-editor/title-tags-datatable-filters.ts","../../../resources/client/admin/title-tags/title-tags-editor/title-tags-datatable-page.tsx","../../../resources/client/admin/lists/lists-datatable-columns.tsx","../../../resources/client/admin/lists/lists-datatable-page.tsx","../../../common/resources/client/admin/analytics/report-date-selector.tsx","../../../common/resources/client/ui/buttons/button-group.tsx","../../../common/resources/client/icons/material/TrendingUp.tsx","../../../common/resources/client/icons/material/TrendingDown.tsx","../../../common/resources/client/charts/chart-layout.tsx","../../../common/resources/client/charts/chart-loading-indicator.tsx","../../../common/resources/client/charts/base-chart.tsx","../../../common/resources/client/charts/data/format-report-data.ts","../../../common/resources/client/charts/chart-colors.tsx","../../../common/resources/client/charts/line-chart.tsx","../../../common/resources/client/charts/polar-area-chart.tsx","../../../common/resources/client/charts/bar-chart.tsx","../../../common/resources/client/admin/analytics/geo-chart/use-google-geo-chart.ts","../../../common/resources/client/i18n/formatted-country-name.tsx","../../../common/resources/client/admin/analytics/geo-chart/geo-chart.tsx","../../../common/resources/client/admin/analytics/visitors-report-charts.tsx","../../../common/resources/client/icons/material/TrendingFlat.tsx","../../../common/resources/client/admin/analytics/admin-header-report.tsx","../../../common/resources/client/admin/analytics/use-admin-report.ts","../../../resources/client/admin/reports/mtdb-admin-report-page.tsx","../../../resources/client/admin/reports/insights/insights-report-row.tsx","../../../resources/client/admin/reports/requests/use-insights-report.ts","../../../resources/client/admin/reports/insights/insights-charts-context.ts","../../../resources/client/admin/reports/insights/insights-async-chart.tsx","../../../resources/client/admin/reports/insights/insights-plays-chart.tsx","../../../resources/client/admin/reports/insights/insights-devices-chart.tsx","../../../common/resources/client/icons/material/Info.tsx","../../../resources/client/admin/reports/top-models-chart-layout.tsx","../../../resources/client/admin/reports/insights/insights-series-chart.tsx","../../../resources/client/admin/reports/insights/insights-movies-chart.tsx","../../../resources/client/admin/reports/insights/insights-videos-chart.tsx","../../../resources/client/admin/reports/insights/insights-users-chart.tsx","../../../resources/client/admin/reports/insights/insights-locations-chart.tsx","../../../resources/client/admin/reports/insights/insights-platforms-chart.tsx","../../../resources/client/admin/reports/admin-insights-report.tsx","../../../resources/client/admin/reports/admin-visitors-report.tsx","../../../resources/client/admin/reports/model-insights-page-layout.tsx","../../../resources/client/admin/reports/insights/insights-seasons-chart.tsx","../../../resources/client/admin/reports/insights/insights-episodes-chart.tsx","../../../resources/client/admin/reports/pages/title-insights-page.tsx","../../../resources/client/admin/reports/pages/episode-insights-page.tsx","../../../resources/client/admin/reports/pages/season-insights-page.tsx","../../../resources/client/admin/reports/pages/video-insights-page.tsx","../../../resources/client/admin/app-admin-routes.tsx","../../../common/resources/client/admin/custom-pages/requests/use-update-custom-page.ts","../../../common/resources/client/admin/custom-pages/edit-custom-page.tsx","../../../common/resources/client/admin/custom-pages/requests/use-create-custom-page.ts","../../../common/resources/client/admin/custom-pages/create-custom-page.tsx","../../../common/resources/client/ui/font-selector/font.svg","../../../common/resources/client/ui/font-selector/font-selector-filters.tsx","../../../common/resources/client/i18n/use-filter.ts","../../../common/resources/client/ui/font-picker/browser-safe-fonts.ts","../../../common/resources/client/ui/font-selector/font-selector-state.ts","../../../common/resources/client/ui/font-selector/font-selector-pagination.tsx","../../../common/resources/client/ui/font-selector/font-selector.tsx","../../../common/resources/client/admin/appearance/sections/themes/theme-font-panel.tsx","../../../common/resources/client/admin/appearance/sections/themes/theme-radius-panel.tsx","../../../common/resources/client/admin/logging/logs-page.tsx","../../../common/resources/client/admin/logging/schedule/use-rerurun-scheduled-command.tsx","../../../common/resources/client/icons/material/EventRepeat.tsx","../../../common/resources/client/admin/logging/schedule/schedule-datatable-columns.tsx","../../../common/resources/client/admin/logging/schedule/timeline.svg","../../../common/resources/client/icons/material/Download.tsx","../../../common/resources/client/admin/logging/schedule/schedule-log-datatable.tsx","../../../common/resources/client/admin/logging/error/bug-fixing.svg","../../../common/resources/client/admin/logging/error/error-log-datatable-columns.tsx","../../../common/resources/client/admin/logging/error/error-log-entry-dialog.tsx","../../../common/resources/client/admin/logging/error/use-delete-error-log.ts","../../../common/resources/client/admin/logging/error/error-log-datatable.tsx","../../../common/resources/client/admin/logging/outgoing-email/opened.svg","../../../common/resources/client/admin/logging/outgoing-email/use-outgoing-email-log-item-with-mime.ts","../../../common/resources/client/admin/logging/outgoing-email/outgoing-email-log-entry-dialog.tsx","../../../common/resources/client/admin/logging/outgoing-email/outgoing-email-log-datatable-columns.tsx","../../../common/resources/client/admin/logging/outgoing-email/outgoing-email-log-datatable-filters.tsx","../../../common/resources/client/admin/logging/outgoing-email/outgoing-email-log-datatable.tsx","../../../common/resources/client/admin/admin-routes.tsx"],"sourcesContent":["import clsx from 'clsx';\nimport React from 'react';\nimport {CustomMenu} from '../menus/custom-menu';\nimport {Trans} from '../i18n/trans';\nimport {useSettings} from '../core/settings/use-settings';\n\ninterface Props {\n className?: string;\n isCompactMode?: boolean;\n}\nexport function AdminSidebar({className, isCompactMode}: Props) {\n const {version} = useSettings();\n return (\n \n to === '/admin'}\n menu=\"admin-sidebar\"\n orientation=\"vertical\"\n onlyShowIcons={isCompactMode}\n itemClassName={({isActive}) =>\n clsx(\n 'block w-full rounded-button py-12 px-16',\n isActive\n ? 'bg-primary/6 text-primary font-semibold'\n : 'hover:bg-hover',\n )\n }\n gap=\"gap-8\"\n />\n {!isCompactMode && (\n
\n \n
\n )}\n \n );\n}\n","import {createContext} from 'react';\n\nexport type DashboardSidenavStatus = 'open' | 'closed' | 'compact';\n\nexport interface DashboardContextValue {\n leftSidenavStatus: DashboardSidenavStatus;\n setLeftSidenavStatus: (status: DashboardSidenavStatus) => void;\n rightSidenavStatus: DashboardSidenavStatus;\n setRightSidenavStatus: (status: DashboardSidenavStatus) => void;\n isMobileMode: boolean | null;\n leftSidenavCanBeCompact?: boolean;\n name: string;\n}\n\nexport const DashboardLayoutContext = createContext(\n null!\n);\n","import {useEffect} from 'react';\n\nexport function useBlockBodyOverflow(disable: boolean = false) {\n useEffect(() => {\n if (disable) {\n document.documentElement.classList.remove('no-page-overflow');\n } else {\n document.documentElement.classList.add('no-page-overflow');\n }\n return () => {\n document.documentElement.classList.remove('no-page-overflow');\n };\n }, [disable]);\n}\n","import {ComponentPropsWithoutRef, useCallback, useMemo} from 'react';\nimport {\n DashboardLayoutContext,\n DashboardSidenavStatus,\n} from './dashboard-layout-context';\nimport {Underlay} from '../overlays/underlay';\nimport {AnimatePresence} from 'framer-motion';\nimport {useControlledState} from '@react-stately/utils';\nimport {useMediaQuery} from '../../utils/hooks/use-media-query';\nimport {\n getFromLocalStorage,\n setInLocalStorage,\n} from '../../utils/hooks/local-storage';\nimport {useBlockBodyOverflow} from '../../utils/hooks/use-block-body-overflow';\nimport clsx from 'clsx';\n\ninterface DashboardLayoutProps extends ComponentPropsWithoutRef<'div'> {\n name: string;\n leftSidenavCanBeCompact?: boolean;\n leftSidenavStatus?: DashboardSidenavStatus;\n onLeftSidenavChange?: (status: DashboardSidenavStatus) => void;\n rightSidenavStatus?: DashboardSidenavStatus;\n initialRightSidenavStatus?: DashboardSidenavStatus;\n onRightSidenavChange?: (status: DashboardSidenavStatus) => void;\n height?: string;\n gridClassName?: string;\n blockBodyOverflow?: boolean;\n}\nexport function DashboardLayout({\n children,\n leftSidenavStatus: leftSidenav,\n onLeftSidenavChange,\n rightSidenavStatus: rightSidenav,\n initialRightSidenavStatus,\n onRightSidenavChange,\n name,\n leftSidenavCanBeCompact,\n height = 'h-screen',\n className,\n gridClassName = 'dashboard-grid',\n blockBodyOverflow = true,\n ...domProps\n}: DashboardLayoutProps) {\n useBlockBodyOverflow(!blockBodyOverflow);\n const isMobile = useMediaQuery('(max-width: 1024px)');\n\n const isCompactModeInitially = useMemo(() => {\n return !name ? false : getFromLocalStorage(`${name}.sidenav.compact`);\n }, [name]);\n const defaultLeftSidenavStatus = isCompactModeInitially ? 'compact' : 'open';\n const [leftSidenavStatus, setLeftSidenavStatus] = useControlledState(\n leftSidenav,\n isMobile ? 'closed' : defaultLeftSidenavStatus,\n onLeftSidenavChange,\n );\n\n const rightSidenavStatusDefault = useMemo(() => {\n if (isMobile) {\n return 'closed';\n }\n if (initialRightSidenavStatus != null) {\n return initialRightSidenavStatus;\n }\n const userSelected = getFromLocalStorage(\n `${name}.sidenav.right.position`,\n 'open',\n );\n if (userSelected != null) {\n return userSelected;\n }\n return initialRightSidenavStatus || 'closed';\n }, [isMobile, name, initialRightSidenavStatus]);\n const [rightSidenavStatus, _setRightSidenavStatus] = useControlledState(\n rightSidenav,\n rightSidenavStatusDefault,\n onRightSidenavChange,\n );\n const setRightSidenavStatus = useCallback(\n (status: DashboardSidenavStatus) => {\n _setRightSidenavStatus(status);\n setInLocalStorage(`${name}.sidenav.right.position`, status);\n },\n [_setRightSidenavStatus, name],\n );\n\n const shouldShowUnderlay =\n isMobile && (leftSidenavStatus === 'open' || rightSidenavStatus === 'open');\n\n return (\n \n \n {children}\n \n {shouldShowUnderlay && (\n {\n setLeftSidenavStatus('closed');\n setRightSidenavStatus('closed');\n }}\n />\n )}\n \n \n \n );\n}\n","import {cloneElement, ReactElement} from 'react';\nimport clsx from 'clsx';\n\ninterface DashboardContentProps {\n children: ReactElement<{className: string}>;\n isScrollable?: boolean;\n}\nexport function DashboardContent({\n children,\n isScrollable = true,\n}: DashboardContentProps) {\n return cloneElement(children, {\n className: clsx(\n children.props.className,\n isScrollable && 'overflow-y-auto stable-scrollbar',\n 'dashboard-grid-content'\n ),\n });\n}\n","import clsx from 'clsx';\nimport {m} from 'framer-motion';\nimport {cloneElement, ReactElement, useContext} from 'react';\nimport {DashboardLayoutContext} from './dashboard-layout-context';\n\nexport interface DashboardSidenavChildrenProps {\n className?: string;\n isCompactMode?: boolean;\n}\n\nexport interface SidenavProps {\n className?: string;\n children: ReactElement;\n position?: 'left' | 'right';\n size?: 'sm' | 'md' | 'lg' | string;\n mode?: 'overlay';\n // absolute will place sidenav between navbar/footer, fixed will overlay it over nav/footer.\n overlayPosition?: 'absolute' | 'fixed';\n display?: 'flex' | 'block';\n overflow?: string;\n forceClosed?: boolean;\n}\nexport function DashboardSidenav({\n className,\n position,\n children,\n size = 'md',\n mode,\n overlayPosition = 'fixed',\n display = 'flex',\n overflow = 'overflow-hidden',\n forceClosed = false,\n}: SidenavProps) {\n const {\n isMobileMode,\n leftSidenavStatus,\n setLeftSidenavStatus,\n rightSidenavStatus,\n setRightSidenavStatus,\n } = useContext(DashboardLayoutContext);\n const status = position === 'left' ? leftSidenavStatus : rightSidenavStatus;\n const isOverlayMode = isMobileMode || mode === 'overlay';\n\n const variants = {\n open: {display, width: null as any},\n compact: {\n display,\n width: null as any,\n },\n closed: {\n width: 0,\n transitionEnd: {\n display: 'none',\n },\n },\n };\n\n const sizeClassName = getSize(status === 'compact' ? 'compact' : size);\n\n return (\n {\n // close sidenav when user clicks a link or button on mobile\n const target = e.target as HTMLElement;\n if (isMobileMode && (target.closest('button') || target.closest('a'))) {\n setLeftSidenavStatus('closed');\n setRightSidenavStatus('closed');\n }\n }}\n className={clsx(\n className,\n position === 'left'\n ? 'dashboard-grid-sidenav-left'\n : 'dashboard-grid-sidenav-right',\n 'will-change-[width]',\n overflow,\n sizeClassName,\n isOverlayMode && `${overlayPosition} bottom-0 top-0 z-20 shadow-2xl`,\n isOverlayMode && position === 'left' && 'left-0',\n isOverlayMode && position === 'right' && 'right-0',\n )}\n >\n {cloneElement(children, {\n className: clsx(\n children.props.className,\n 'w-full h-full',\n status === 'compact' && 'compact-scrollbar',\n ),\n isCompactMode: status === 'compact',\n })}\n \n );\n}\n\nfunction getSize(size: SidenavProps['size'] | 'compact'): string {\n switch (size) {\n case 'compact':\n return 'w-80';\n case 'sm':\n return 'w-224';\n case 'md':\n return 'w-240';\n case 'lg':\n return 'w-288';\n default:\n return size || '';\n }\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const MenuOpenIcon = createSvgIcon(\n \n, 'MenuOpenOutlined');\n","import {Navbar, NavbarProps} from '../navigation/navbar/navbar';\nimport {IconButton} from '../buttons/icon-button';\nimport React, {useContext} from 'react';\nimport clsx from 'clsx';\nimport {DashboardLayoutContext} from './dashboard-layout-context';\nimport {setInLocalStorage} from '../../utils/hooks/local-storage';\nimport {MenuOpenIcon} from '@common/icons/material/MenuOpen';\n\nexport interface DashboardNavbarProps\n extends Omit {\n hideToggleButton?: boolean;\n}\nexport function DashboardNavbar({\n children,\n className,\n hideToggleButton,\n ...props\n}: DashboardNavbarProps) {\n const {\n isMobileMode,\n leftSidenavStatus,\n setLeftSidenavStatus,\n name,\n leftSidenavCanBeCompact,\n } = useContext(DashboardLayoutContext);\n\n const shouldToggleCompactMode = leftSidenavCanBeCompact && !isMobileMode;\n const shouldShowToggle =\n !hideToggleButton && (isMobileMode || leftSidenavCanBeCompact);\n\n const handleToggle = () => {\n setLeftSidenavStatus(leftSidenavStatus === 'open' ? 'closed' : 'open');\n };\n\n const handleCompactModeToggle = () => {\n const newStatus = leftSidenavStatus === 'compact' ? 'open' : 'compact';\n setInLocalStorage(`${name}.sidenav.compact`, newStatus === 'compact');\n setLeftSidenavStatus(newStatus);\n };\n\n return (\n {\n if (shouldToggleCompactMode) {\n handleCompactModeToggle();\n } else {\n handleToggle();\n }\n }}\n >\n \n \n ) : undefined\n }\n {...props}\n >\n {children}\n \n );\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 AdminSetupAlert {\n title: string;\n description: string;\n}\n\ninterface Response extends BackendResponse {\n alerts: AdminSetupAlert[];\n}\n\nexport function useAdminSetupAlerts() {\n return useQuery({\n queryKey: ['admin-setup-alerts'],\n queryFn: () => fetchAlerts(),\n });\n}\n\nfunction fetchAlerts() {\n return apiClient\n .get(`admin/setup-alerts`)\n .then(response => response.data);\n}\n","import {Outlet} from 'react-router-dom';\nimport {AdminSidebar} from './admin-sidebar';\nimport {DashboardLayout} from '../ui/layout/dashboard-layout';\nimport {DashboardContent} from '../ui/layout/dashboard-content';\nimport {DashboardSidenav} from '../ui/layout/dashboard-sidenav';\nimport {DashboardNavbar} from '../ui/layout/dashboard-navbar';\nimport {\n AdminSetupAlert,\n useAdminSetupAlerts,\n} from '@common/admin/use-admin-setup-alerts';\nimport {SectionHelper} from '@common/ui/section-helper';\nimport {ErrorIcon} from '@common/icons/material/Error';\nimport {\n setInLocalStorage,\n useLocalStorage,\n} from '@common/utils/hooks/local-storage';\n\nexport function AdminLayout() {\n return (\n \n \n \n \n \n \n
\n \n \n
\n
\n
\n );\n}\n\nfunction SetupAlertsList() {\n const {data} = useAdminSetupAlerts();\n const [dismissValue] = useLocalStorage<{\n timestamp: number;\n } | null>('admin-setup-alert-dismissed', null);\n\n // show alert if 1 day passed since last dismiss\n const shouldShowAlert =\n !dismissValue || Date.now() - dismissValue.timestamp > 86400000;\n\n if (!data?.alerts.length || !shouldShowAlert) {\n return null;\n }\n\n return (\n
\n \n
\n );\n}\n\ninterface SetupAlertProps {\n alert: AdminSetupAlert;\n}\nfunction SetupAlert({alert}: SetupAlertProps) {\n const description = (\n
\n );\n return (\n }\n onClose={() => {\n setInLocalStorage('admin-setup-alert-dismissed', {\n timestamp: Date.now(),\n });\n }}\n key={alert.title}\n title={alert.title}\n description={description}\n color=\"neutral\"\n />\n );\n}\n","import {\n BackendFilter,\n DatePickerFilterControl,\n FilterControlType,\n FilterOperator,\n} from './backend-filter';\nimport {\n DateRangePreset,\n DateRangePresets,\n} from '../../ui/forms/input-field/date/date-range-picker/dialog/date-range-presets';\nimport {message} from '../../i18n/message';\nimport {dateRangeToAbsoluteRange} from '../../ui/forms/input-field/date/date-range-picker/form-date-range-picker';\nimport {PartialWithRequired} from '@common/utils/ts/partial-with-required';\n\nexport function timestampFilter(\n options: PartialWithRequired<\n BackendFilter,\n 'key' | 'label'\n >\n): BackendFilter {\n return {\n ...options,\n defaultOperator: FilterOperator.between,\n control: {\n type: FilterControlType.DateRangePicker,\n defaultValue:\n options.control?.defaultValue ||\n dateRangeToAbsoluteRange(\n (DateRangePresets[3] as Required).getRangeValue()\n ),\n },\n };\n}\n\nexport function createdAtFilter(\n options: Partial>\n): BackendFilter {\n return timestampFilter({\n key: 'created_at',\n label: message('Date created'),\n ...options,\n });\n}\n\nexport function updatedAtFilter(\n options: Partial>\n): BackendFilter {\n return timestampFilter({\n key: 'updated_at',\n label: message('Last updated'),\n ...options,\n });\n}\n","import {\n BackendFilter,\n FilterControlType,\n FilterOperator,\n} from '../../datatable/filters/backend-filter';\nimport {\n createdAtFilter,\n updatedAtFilter,\n} from '../../datatable/filters/timestamp-filters';\nimport {message} from '../../i18n/message';\n\nexport const UserDatatableFilters: BackendFilter[] = [\n {\n key: 'email_verified_at',\n label: message('Email'),\n description: message('Email verification status'),\n defaultOperator: FilterOperator.ne,\n control: {\n type: FilterControlType.Select,\n defaultValue: '01',\n options: [\n {\n key: '01',\n label: message('is confirmed'),\n value: {value: null, operator: FilterOperator.ne},\n },\n {\n key: '02',\n label: message('is not confirmed'),\n value: {value: null, operator: FilterOperator.eq},\n },\n ],\n },\n },\n createdAtFilter({\n description: message('Date user registered or was created'),\n }),\n updatedAtFilter({\n description: message('Date user was last updated'),\n }),\n {\n key: 'subscriptions',\n label: message('Subscription'),\n description: message('Whether user is subscribed or not'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.Select,\n defaultValue: '01',\n options: [\n {\n key: '01',\n label: message('is subscribed'),\n value: {value: '*', operator: FilterOperator.has},\n },\n {\n key: '02',\n label: message('is not subscribed'),\n value: {value: '*', operator: FilterOperator.doesntHave},\n },\n ],\n },\n },\n];\n","import React, {useContext} from 'react';\nimport {GetDatatableDataParams} from '../requests/paginated-resources';\nimport {UseQueryResult} from '@tanstack/react-query';\nimport {PaginatedBackendResponse} from '../../http/backend-response/pagination-response';\n\nexport interface DataTableContextValue {\n selectedRows: (string | number)[];\n setSelectedRows: (keys: (string | number)[]) => void;\n endpoint: string;\n params: GetDatatableDataParams;\n setParams: (value: GetDatatableDataParams) => void;\n query: UseQueryResult & A, unknown>;\n}\n\nexport const DataTableContext = React.createContext(\n null!,\n);\n\nexport function useDataTable() {\n return useContext(DataTableContext) as DataTableContextValue;\n}\n","import {UseQueryResult} from '@tanstack/react-query';\nimport {\n hasNextPage,\n LengthAwarePaginationResponse,\n PaginatedBackendResponse,\n} from '../http/backend-response/pagination-response';\nimport {useNumberFormatter} from '../i18n/use-number-formatter';\nimport {Select} from '../ui/forms/select/select';\nimport {Trans} from '../i18n/trans';\nimport {Item} from '../ui/forms/listbox/item';\nimport {IconButton} from '../ui/buttons/icon-button';\nimport {KeyboardArrowLeftIcon} from '../icons/material/KeyboardArrowLeft';\nimport {KeyboardArrowRightIcon} from '../icons/material/KeyboardArrowRight';\nimport React from 'react';\nimport {useIsMobileMediaQuery} from '../utils/hooks/is-mobile-media-query';\nimport clsx from 'clsx';\n\nconst defaultPerPage = 15;\nconst perPageOptions = [{key: 10}, {key: 15}, {key: 20}, {key: 50}, {key: 100}];\n\ntype DataTablePaginationFooterProps = {\n query: UseQueryResult, unknown>;\n onPerPageChange?: (perPage: number) => void;\n onPageChange?: (page: number) => void;\n className?: string;\n};\nexport function DataTablePaginationFooter({\n query,\n onPerPageChange,\n onPageChange,\n className,\n}: DataTablePaginationFooterProps) {\n const isMobile = useIsMobileMediaQuery();\n const numberFormatter = useNumberFormatter();\n const pagination = query.data\n ?.pagination as LengthAwarePaginationResponse;\n\n if (!pagination) return null;\n\n const perPageSelect = onPerPageChange ? (\n }\n selectedValue={pagination.per_page || defaultPerPage}\n onSelectionChange={value => onPerPageChange(value as number)}\n >\n {perPageOptions.map(option => (\n \n {option.key}\n \n ))}\n \n ) : null;\n\n return (\n \n {!isMobile && perPageSelect}\n {pagination.from && pagination.to && 'total' in pagination ? (\n
\n \n
\n ) : null}\n
\n {\n onPageChange?.(pagination?.current_page - 1);\n }}\n >\n \n \n {\n onPageChange?.(pagination?.current_page + 1);\n }}\n >\n \n \n
\n \n );\n}\n","import React, {ComponentPropsWithoutRef, ReactNode} from 'react';\nimport {BackendFilter} from './filters/backend-filter';\nimport {useTrans} from '../i18n/use-trans';\nimport {TextField} from '../ui/forms/input-field/text-field/text-field';\nimport {SearchIcon} from '../icons/material/Search';\nimport {AddFilterButton} from './filters/add-filter-button';\nimport {MessageDescriptor} from '@common/i18n/message-descriptor';\nimport {message} from '@common/i18n/message';\n\ninterface Props {\n actions?: ReactNode;\n filters?: BackendFilter[];\n filtersLoading?: boolean;\n searchPlaceholder?: MessageDescriptor;\n searchValue?: string;\n onSearchChange: (value: string) => void;\n}\nexport function DataTableHeader({\n actions,\n filters,\n filtersLoading,\n searchPlaceholder = message('Type to search...'),\n searchValue = '',\n onSearchChange,\n}: Props) {\n const {trans} = useTrans();\n return (\n \n }\n value={searchValue}\n onChange={e => {\n onSearchChange(e.target.value);\n }}\n />\n {filters && (\n \n )}\n {actions}\n \n );\n}\n\ninterface AnimatedHeaderProps extends ComponentPropsWithoutRef<'div'> {\n children: ReactNode;\n}\nexport function HeaderLayout({children, ...domProps}: AnimatedHeaderProps) {\n return (\n \n {children}\n \n );\n}\n","import {Trans} from '@common/i18n/trans';\nimport React, {ReactNode} from 'react';\nimport {HeaderLayout} from '@common/datatable/data-table-header';\n\ninterface Props {\n actions?: ReactNode;\n selectedItemsCount: number;\n}\nexport function SelectedStateDatatableHeader({\n actions,\n selectedItemsCount,\n}: Props) {\n return (\n \n
\n \n
\n {actions}\n
\n );\n}\n","import React, {\n cloneElement,\n ComponentProps,\n ReactElement,\n ReactNode,\n useState,\n} from 'react';\nimport {TableDataItem} from '../ui/tables/types/table-data-item';\nimport {BackendFilter} from './filters/backend-filter';\nimport {MessageDescriptor} from '../i18n/message-descriptor';\nimport {ColumnConfig} from './column-config';\nimport {useTrans} from '../i18n/use-trans';\nimport {useBackendFilterUrlParams} from './filters/backend-filter-url-params';\nimport {\n GetDatatableDataParams,\n useDatatableData,\n} from './requests/paginated-resources';\nimport {DataTableContext} from './page/data-table-context';\nimport {AnimatePresence, m} from 'framer-motion';\nimport {ProgressBar} from '../ui/progress/progress-bar';\nimport {Table, TableProps} from '../ui/tables/table';\nimport {DataTablePaginationFooter} from './data-table-pagination-footer';\nimport {DataTableHeader} from './data-table-header';\nimport {FilterList} from './filters/filter-list/filter-list';\nimport {SelectedStateDatatableHeader} from '@common/datatable/selected-state-datatable-header';\nimport clsx from 'clsx';\nimport {useIsMobileMediaQuery} from '@common/utils/hooks/is-mobile-media-query';\nimport {BackendFiltersUrlKey} from '@common/datatable/filters/backend-filters-url-key';\nimport {opacityAnimation} from '@common/ui/animation/opacity-animation';\nimport {FilterListSkeleton} from '@common/datatable/filters/filter-list/filter-list-skeleton';\n\nexport interface DataTableProps {\n filters?: BackendFilter[];\n filtersLoading?: boolean;\n columns: ColumnConfig[];\n searchPlaceholder?: MessageDescriptor;\n queryParams?: Record;\n endpoint: string;\n resourceName?: ReactNode;\n emptyStateMessage: ReactElement<{isFiltering: boolean}>;\n actions?: ReactNode;\n enableSelection?: boolean;\n selectionStyle?: TableProps['selectionStyle'];\n selectedActions?: ReactNode;\n onRowAction?: TableProps['onAction'];\n tableDomProps?: ComponentProps<'table'>;\n children?: ReactNode;\n collapseTableOnMobile?: boolean;\n cellHeight?: string;\n}\nexport function DataTable({\n filters,\n filtersLoading,\n columns,\n searchPlaceholder,\n queryParams,\n endpoint,\n actions,\n selectedActions,\n emptyStateMessage,\n tableDomProps,\n onRowAction,\n enableSelection = true,\n selectionStyle = 'checkbox',\n children,\n cellHeight,\n collapseTableOnMobile = true,\n}: DataTableProps) {\n const isMobile = useIsMobileMediaQuery();\n const {trans} = useTrans();\n const {encodedFilters} = useBackendFilterUrlParams(filters);\n const [params, setParams] = useState({perPage: 15});\n const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]);\n const query = useDatatableData(\n endpoint,\n {\n ...params,\n ...queryParams,\n [BackendFiltersUrlKey]: encodedFilters,\n },\n undefined,\n () => setSelectedRows([]),\n );\n\n const isFiltering = !!(params.query || params.filters || encodedFilters);\n const pagination = query.data?.pagination;\n\n return (\n \n {children}\n \n {selectedRows.length ? (\n \n ) : (\n setParams({...params, query})}\n actions={actions}\n filters={filters}\n filtersLoading={filtersLoading}\n key=\"default\"\n />\n )}\n \n\n {filters && (\n
\n \n {filtersLoading && encodedFilters ? (\n \n ) : (\n \n \n \n )}\n \n
\n )}\n\n \n {query.isFetching && (\n \n )}\n\n
\n {\n setParams({...params, ...descriptor});\n }}\n selectedRows={selectedRows}\n enableSelection={enableSelection}\n selectionStyle={selectionStyle}\n onSelectionChange={setSelectedRows}\n onAction={onRowAction}\n collapseOnMobile={collapseTableOnMobile}\n cellHeight={cellHeight}\n />\n
\n\n {(query.isFetched || query.isPlaceholderData) &&\n !pagination?.data.length ? (\n
\n {cloneElement(emptyStateMessage, {\n isFiltering,\n })}\n
\n ) : undefined}\n\n setParams({...params, page})}\n onPerPageChange={perPage => setParams({...params, perPage})}\n />\n \n \n );\n}\n","import React, {ReactElement, ReactNode, useId} from 'react';\nimport {TableDataItem} from '../../ui/tables/types/table-data-item';\nimport {DataTable, DataTableProps} from '../data-table';\nimport {TableProps} from '../../ui/tables/table';\nimport {StaticPageTitle} from '../../seo/static-page-title';\nimport {MessageDescriptor} from '../../i18n/message-descriptor';\nimport clsx from 'clsx';\n\ninterface Props extends DataTableProps {\n title?: ReactElement;\n headerContent?: ReactNode;\n headerItemsAlign?: string;\n enableSelection?: boolean;\n onRowAction?: TableProps['onAction'];\n padding?: string;\n className?: string;\n}\nexport function DataTablePage({\n title,\n headerContent,\n headerItemsAlign = 'items-end',\n className,\n padding,\n ...dataTableProps\n}: Props) {\n const titleId = useId();\n\n return (\n
\n {title && (\n \n {title}\n

\n {title}\n

\n {headerContent}\n
\n )}\n\n \n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient, queryClient} from '../../http/query-client';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {toast} from '../../ui/toast/toast';\nimport {DatatableDataQueryKey} from './paginated-resources';\nimport {useDataTable} from '../page/data-table-context';\nimport {message} from '../../i18n/message';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\nimport {Key} from 'react';\n\ninterface Response extends BackendResponse {\n //\n}\n\nexport function useDeleteSelectedRows() {\n const {endpoint, selectedRows, setSelectedRows} = useDataTable();\n return useMutation({\n mutationFn: () => deleteSelectedRows(endpoint, selectedRows),\n onSuccess: async () => {\n await queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey(endpoint),\n });\n toast(\n message('Deleted [one 1 record|other :count records]', {\n values: {count: selectedRows.length},\n }),\n );\n setSelectedRows([]);\n },\n onError: err =>\n showHttpErrorToast(err, message('Could not delete records')),\n });\n}\n\nfunction deleteSelectedRows(endpoint: string, ids: Key[]): Promise {\n return apiClient.delete(`${endpoint}/${ids.join(',')}`).then(r => r.data);\n}\n","import {Button} from '../../ui/buttons/button';\nimport {Trans} from '../../i18n/trans';\nimport {ConfirmationDialog} from '../../ui/overlays/dialog/confirmation-dialog';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport React from 'react';\nimport {useDeleteSelectedRows} from '../requests/delete-selected-rows';\nimport {useDataTable} from './data-table-context';\nimport {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';\nimport {errorStatusIs} from '@common/utils/http/error-status-is';\n\nexport function DeleteSelectedItemsAction() {\n return (\n \n \n \n \n );\n}\n\nfunction DeleteItemsDialog() {\n const deleteSelectedRows = useDeleteSelectedRows();\n const {selectedRows, setSelectedRows} = useDataTable();\n const {close} = useDialogContext();\n return (\n \n }\n body={\n \n }\n confirm={}\n isDanger\n onConfirm={() => {\n deleteSelectedRows.mutate(undefined, {\n onSuccess: () => close(),\n onError: err => {\n if (errorStatusIs(err, 422)) {\n setSelectedRows([]);\n close();\n }\n },\n });\n }}\n />\n );\n}\n","import React, {ReactNode} from 'react';\nimport {IllustratedMessage} from '../../ui/images/illustrated-message';\nimport {SvgImage} from '../../ui/images/svg-image/svg-image';\nimport {Trans} from '../../i18n/trans';\nimport {useIsMobileMediaQuery} from '../../utils/hooks/is-mobile-media-query';\n\nexport interface DataTableEmptyStateMessageProps {\n isFiltering?: boolean;\n title: ReactNode;\n filteringTitle?: ReactNode;\n image: string;\n size?: 'sm' | 'md';\n className?: string;\n}\nexport function DataTableEmptyStateMessage({\n isFiltering,\n title,\n filteringTitle,\n image,\n size,\n className,\n}: DataTableEmptyStateMessageProps) {\n const isMobile = useIsMobileMediaQuery();\n if (!size) {\n size = isMobile ? 'sm' : 'md';\n }\n\n // allow user to disable filtering message variation by not passing in \"filteringTitle\"\n return (\n }\n title={isFiltering && filteringTitle ? filteringTitle : title}\n description={\n isFiltering && filteringTitle ? (\n \n ) : undefined\n }\n />\n );\n}\n","export default \"__VITE_ASSET__d109d853__\"","import {AddIcon} from '../icons/material/Add';\nimport {Button} from '../ui/buttons/button';\nimport React, {ReactElement, ReactNode} from 'react';\nimport {useIsMobileMediaQuery} from '../utils/hooks/is-mobile-media-query';\nimport {IconButton} from '../ui/buttons/icon-button';\nimport {To} from 'react-router-dom';\nimport {ButtonBaseProps} from '../ui/buttons/button-base';\n\nexport interface DataTableAddItemButtonProps {\n children: ReactNode;\n to?: To;\n href?: string;\n download?: boolean | string;\n elementType?: ButtonBaseProps['elementType'];\n onClick?: ButtonBaseProps['onClick'];\n icon?: ReactElement;\n disabled?: boolean;\n}\nexport const DataTableAddItemButton = React.forwardRef<\n HTMLButtonElement,\n DataTableAddItemButtonProps\n>(\n (\n {children, to, elementType, onClick, href, download, icon, disabled},\n ref,\n ) => {\n const isMobile = useIsMobileMediaQuery();\n\n if (isMobile) {\n return (\n \n {icon || }\n \n );\n }\n\n return (\n }\n variant=\"flat\"\n color=\"primary\"\n size=\"sm\"\n to={to}\n href={href}\n download={download}\n elementType={elementType}\n onClick={onClick}\n disabled={disabled}\n >\n {children}\n \n );\n },\n);\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const FileDownloadIcon = createSvgIcon(\n \n, 'FileDownloadOutlined');\n","import {apiClient} from '../../http/query-client';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {useMutation} from '@tanstack/react-query';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {\n downloadPath?: string;\n result?: 'jobQueued';\n}\n\nexport type ExportCsvPayload = Record;\n\nexport function useExportCsv(endpoint: string) {\n return useMutation({\n mutationFn: (payload?: ExportCsvPayload) => exportCsv(endpoint, payload),\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction exportCsv(\n endpoint: string,\n payload: ExportCsvPayload | undefined,\n): Promise {\n return apiClient.post(endpoint, payload).then(r => r.data);\n}\n","export function downloadFileFromUrl(url: string, name?: string) {\n const link = document.createElement('a');\n link.href = url;\n if (name) link.download = name;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n}\n","import {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\nimport {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {Button} from '../../ui/buttons/button';\nimport {Trans} from '../../i18n/trans';\n\nexport function CsvExportInfoDialog() {\n const {close} = useDialogContext();\n return (\n \n \n \n \n \n \n \n \n \n \n \n );\n}\n","import {IconButton} from '../../ui/buttons/icon-button';\nimport {FileDownloadIcon} from '../../icons/material/FileDownload';\nimport React, {Fragment, useState} from 'react';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {ExportCsvPayload, useExportCsv} from '../requests/use-export-csv';\nimport {downloadFileFromUrl} from '../../uploads/utils/download-file-from-url';\nimport {CsvExportInfoDialog} from './csv-export-info-dialog';\n\ninterface DataTableExportCsvButtonProps {\n endpoint: string;\n payload?: ExportCsvPayload;\n}\nexport function DataTableExportCsvButton({\n endpoint,\n payload,\n}: DataTableExportCsvButtonProps) {\n const [dialogIsOpen, setDialogIsOpen] = useState(false);\n const exportCsv = useExportCsv(endpoint);\n\n return (\n \n {\n exportCsv.mutate(payload, {\n onSuccess: response => {\n if (response.downloadPath) {\n downloadFileFromUrl(response.downloadPath);\n } else {\n setDialogIsOpen(true);\n }\n },\n });\n }}\n >\n \n \n \n \n \n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const PersonOffIcon = createSvgIcon(\n \n, 'PersonOffOutlined');\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {User} from '@common/auth/user';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {toast} from '@common/ui/toast/toast';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {message} from '@common/i18n/message';\n\ninterface Response extends BackendResponse {\n user: User;\n}\n\nexport interface BanUserPayload {\n ban_until?: string;\n permanent?: boolean;\n comment?: string;\n}\n\nexport function useBanUser(\n form: UseFormReturn,\n userId: number,\n) {\n return useMutation({\n mutationFn: (payload: BanUserPayload) => banUser(userId, payload),\n onSuccess: async () => {\n toast(message('User suspended'));\n await queryClient.invalidateQueries({queryKey: ['users']});\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction banUser(userId: number, payload: BanUserPayload): Promise {\n return apiClient.post(`users/${userId}/ban`, payload).then(r => r.data);\n}\n","import {useControlledState} from '@react-stately/utils';\nimport {HTMLAttributes, useCallback, useState} from 'react';\nimport {\n CalendarDate,\n DateValue,\n isSameDay,\n toCalendarDate,\n toZoned,\n ZonedDateTime,\n} from '@internationalized/date';\nimport {useBaseDatePickerState} from '../use-base-date-picker-state';\nimport {useCurrentDateTime} from '@common/i18n/use-current-date-time';\n\nexport type Granularity = 'day' | 'minute';\n\nexport type DatePickerState = BaseDatePickerState;\n\nexport interface BaseDatePickerState {\n timezone: string;\n granularity: Granularity;\n selectedValue: T;\n setSelectedValue: (value: T) => void;\n calendarIsOpen: boolean;\n setCalendarIsOpen: (isOpen: boolean) => void;\n calendarDates: CalendarDate[];\n setCalendarDates: (dates: CalendarDate[]) => void;\n dayIsActive: (day: CalendarDate) => boolean;\n dayIsHighlighted: (day: CalendarDate) => boolean;\n dayIsRangeStart: (day: CalendarDate) => boolean;\n dayIsRangeEnd: (day: CalendarDate) => boolean;\n isPlaceholder: P;\n setIsPlaceholder: (value: P) => void;\n clear: () => void;\n min?: ZonedDateTime;\n max?: ZonedDateTime;\n closeDialogOnSelection: boolean;\n getCellProps: (\n date: CalendarDate,\n isSameMonth: boolean,\n ) => HTMLAttributes;\n}\n\nexport interface DatePickerValueProps {\n value?: V | null | '';\n defaultValue?: V | null;\n onChange?: (value: CV | null) => void;\n min?: DateValue;\n max?: DateValue;\n granularity?: Granularity;\n closeDialogOnSelection?: boolean;\n}\nexport function useDatePickerState(\n props: DatePickerValueProps,\n): BaseDatePickerState {\n const now = useCurrentDateTime();\n const [isPlaceholder, setIsPlaceholder] = useState(\n !props.value && !props.defaultValue,\n );\n\n // if user clears the date, we will want to still keep an\n // instance internally, but return null via \"onChange\" callback\n const setStateValue = props.onChange;\n const [internalValue, setInternalValue] = useControlledState(\n props.value || now,\n props.defaultValue || now,\n value => {\n setIsPlaceholder(false);\n setStateValue?.(value);\n },\n );\n\n const {\n min,\n max,\n granularity,\n timezone,\n calendarIsOpen,\n setCalendarIsOpen,\n closeDialogOnSelection,\n } = useBaseDatePickerState(internalValue, props);\n\n const clear = useCallback(() => {\n setIsPlaceholder(true);\n setInternalValue(now);\n setStateValue?.(null);\n setCalendarIsOpen(false);\n }, [now, setInternalValue, setStateValue, setCalendarIsOpen]);\n\n const [calendarDates, setCalendarDates] = useState(() => {\n return [toCalendarDate(internalValue)];\n });\n\n const setSelectedValue = useCallback(\n (newValue: DateValue) => {\n if (min && newValue.compare(min) < 0) {\n newValue = min;\n } else if (max && newValue.compare(max) > 0) {\n newValue = max;\n }\n\n // preserve time\n const value = internalValue\n ? internalValue.set(newValue)\n : toZoned(newValue, timezone);\n setInternalValue(value);\n setCalendarDates([toCalendarDate(value)]);\n setIsPlaceholder(false);\n },\n [setInternalValue, min, max, internalValue, timezone],\n );\n\n const dayIsActive = useCallback(\n (day: DateValue) => !isPlaceholder && isSameDay(internalValue, day),\n [internalValue, isPlaceholder],\n );\n\n const getCellProps = useCallback(\n (date: DateValue): HTMLAttributes => {\n return {\n onClick: () => {\n setSelectedValue?.(date);\n if (closeDialogOnSelection) {\n setCalendarIsOpen?.(false);\n }\n },\n };\n },\n [setSelectedValue, setCalendarIsOpen, closeDialogOnSelection],\n );\n\n return {\n selectedValue: internalValue,\n setSelectedValue: setInternalValue,\n calendarIsOpen,\n setCalendarIsOpen,\n dayIsActive,\n dayIsHighlighted: () => false,\n dayIsRangeStart: () => false,\n dayIsRangeEnd: () => false,\n getCellProps,\n calendarDates,\n setCalendarDates,\n isPlaceholder,\n clear,\n setIsPlaceholder,\n min,\n max,\n granularity,\n timezone,\n closeDialogOnSelection,\n };\n}\n","import React, {\n ComponentPropsWithoutRef,\n Fragment,\n MouseEvent,\n useRef,\n} from 'react';\nimport {parseAbsoluteToLocal, ZonedDateTime} from '@internationalized/date';\nimport {useController} from 'react-hook-form';\nimport {mergeProps} from '@react-aria/utils';\nimport {\n DatePickerValueProps,\n useDatePickerState,\n} from './use-date-picker-state';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {DateRangeIcon} from '@common/icons/material/DateRange';\nimport {Dialog} from '@common/ui/overlays/dialog/dialog';\nimport {DialogBody} from '@common/ui/overlays/dialog/dialog-body';\nimport {Calendar} from '../calendar/calendar';\nimport {\n DatePickerField,\n DatePickerFieldProps,\n} from '../date-range-picker/date-picker-field';\nimport {DateSegmentList} from '../segments/date-segment-list';\nimport {useDateFormatter} from '@common/i18n/use-date-formatter';\nimport {useTrans} from '@common/i18n/use-trans';\nimport clsx from 'clsx';\nimport {DialogFooter} from '@common/ui/overlays/dialog/dialog-footer';\nimport {Button} from '@common/ui/buttons/button';\nimport {Trans} from '@common/i18n/trans';\nimport {useCurrentDateTime} from '@common/i18n/use-current-date-time';\n\nexport interface DatePickerProps\n extends Omit,\n DatePickerValueProps {}\nexport function DatePicker({showCalendarFooter, ...props}: DatePickerProps) {\n const state = useDatePickerState(props);\n const inputRef = useRef(null);\n const now = useCurrentDateTime();\n\n const footer = showCalendarFooter && (\n {\n state.clear();\n }}\n >\n \n \n }\n >\n {\n state.setSelectedValue(now);\n state.setCalendarIsOpen(false);\n }}\n >\n \n \n \n );\n\n const dialog = (\n \n \n \n \n \n {footer}\n \n \n );\n\n const openOnClick: ComponentPropsWithoutRef<'div'> = {\n onClick: e => {\n e.stopPropagation();\n e.preventDefault();\n if (!isHourSegment(e)) {\n state.setCalendarIsOpen(true);\n } else {\n state.setCalendarIsOpen(false);\n }\n },\n };\n\n return (\n \n \n }\n {...props}\n >\n \n \n {dialog}\n \n );\n}\n\ninterface FormDatePickerProps extends DatePickerProps {\n name: string;\n}\nexport function FormDatePicker(props: FormDatePickerProps) {\n const {min, max} = props;\n const {trans} = useTrans();\n const {format} = useDateFormatter();\n const {\n field: {onChange, onBlur, value = null, ref},\n fieldState: {invalid, error},\n } = useController({\n name: props.name,\n rules: {\n validate: v => {\n if (!v) return;\n const date = parseAbsoluteToLocal(v);\n if (min && date.compare(min) < 0) {\n return trans({\n message: 'Enter a date after :date',\n values: {date: format(v)},\n });\n }\n if (max && date.compare(max) > 0) {\n return trans({\n message: 'Enter a date before :date',\n values: {date: format(v)},\n });\n }\n },\n },\n });\n\n const parsedValue: null | ZonedDateTime = value\n ? parseAbsoluteToLocal(value)\n : null;\n\n const formProps: Partial = {\n onChange: e => {\n onChange(e ? e.toAbsoluteString() : e);\n },\n onBlur,\n value: parsedValue,\n invalid,\n errorMessage: error?.message,\n inputRef: ref,\n };\n\n return ;\n}\n\nfunction isHourSegment(e: MouseEvent): boolean {\n return ['hour', 'minute', 'dayPeriod'].includes(\n (e.currentTarget as HTMLElement).ariaLabel || ''\n );\n}\n","import {Dialog} from '@common/ui/overlays/dialog/dialog';\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 {DialogFooter} from '@common/ui/overlays/dialog/dialog-footer';\nimport {Button} from '@common/ui/buttons/button';\nimport {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';\nimport {Form} from '@common/ui/forms/form';\nimport {useForm} from 'react-hook-form';\nimport {\n BanUserPayload,\n useBanUser,\n} from '@common/admin/users/requests/use-ban-user';\nimport {FormDatePicker} from '@common/ui/forms/input-field/date/date-picker/date-picker';\nimport {User} from '@common/auth/user';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {message} from '@common/i18n/message';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\n\ninterface Props {\n user: User;\n}\nexport function BanUserDialog({user}: Props) {\n const {trans} = useTrans();\n const {close, formId} = useDialogContext();\n const form = useForm({\n defaultValues: {\n permanent: true,\n },\n });\n const isPermanent = form.watch('permanent');\n const banUser = useBanUser(form, user.id);\n return (\n \n \n \n \n \n \n banUser.mutate(values, {onSuccess: () => close()})\n }\n >\n }\n disabled={isPermanent}\n />\n \n \n \n }\n placeholder={trans(message('Optional'))}\n />\n \n \n \n \n \n \n \n \n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {toast} from '@common/ui/toast/toast';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {message} from '@common/i18n/message';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\nexport function useUnbanUser(userId: number) {\n return useMutation({\n mutationFn: () => unbanUser(userId),\n onSuccess: () => {\n toast(message('User unsuspended'));\n queryClient.invalidateQueries({queryKey: ['users']});\n },\n onError: r => showHttpErrorToast(r),\n });\n}\n\nfunction unbanUser(userId: number): Promise {\n return apiClient.delete(`users/${userId}/unban`).then(r => r.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {toast} from '@common/ui/toast/toast';\nimport {apiClient} from '@common/http/query-client';\nimport {message} from '@common/i18n/message';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {User} from '@common/auth/user';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {\n user: User;\n}\n\ninterface Payload {\n userId: string | number;\n}\n\nexport function useImpersonateUser() {\n return useMutation({\n mutationFn: (payload: Payload) => impersonateUser(payload),\n onSuccess: async response => {\n toast(message(`Impersonating User \"${response.user.display_name}\"`));\n window.location.href = '/';\n },\n onError: r => showHttpErrorToast(r),\n });\n}\n\nfunction impersonateUser(payload: Payload) {\n return apiClient\n .post(`admin/users/impersonate/${payload.userId}`, payload)\n .then(r => r.data);\n}\n","import {ColumnConfig} from '@common/datatable/column-config';\nimport {User} from '@common/auth/user';\nimport {Trans} from '@common/i18n/trans';\nimport {NameWithAvatar} from '@common/datatable/column-templates/name-with-avatar';\nimport {CheckIcon} from '@common/icons/material/Check';\nimport {CloseIcon} from '@common/icons/material/Close';\nimport {ChipList} from '@common/ui/forms/input-field/chip-field/chip-list';\nimport {Chip} from '@common/ui/forms/input-field/chip-field/chip';\nimport {Link} from 'react-router-dom';\nimport clsx from 'clsx';\nimport {FormattedDate} from '@common/i18n/formatted-date';\nimport {Tooltip} from '@common/ui/tooltip/tooltip';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {EditIcon} from '@common/icons/material/Edit';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {PersonOffIcon} from '@common/icons/material/PersonOff';\nimport {BanUserDialog} from '@common/admin/users/ban-user-dialog';\nimport React from 'react';\nimport {useUnbanUser} from '@common/admin/users/requests/use-unban-user';\nimport {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';\nimport {useImpersonateUser} from '@common/admin/users/requests/use-impersonate-user';\nimport {LoginIcon} from '@common/icons/material/Login';\n\nexport const userDatatableColumns: ColumnConfig[] = [\n {\n key: 'name',\n allowsSorting: true,\n sortingKey: 'email',\n width: 'flex-3 min-w-200',\n visibleInMode: 'all',\n header: () => ,\n body: user => (\n \n ),\n },\n {\n key: 'subscribed',\n header: () => ,\n width: 'w-96',\n body: user =>\n user.subscriptions?.length ? (\n \n ) : (\n \n ),\n },\n {\n key: 'roles',\n header: () => ,\n body: user => (\n \n {user?.roles?.map(role => (\n \n \n \n \n \n ))}\n \n ),\n },\n {\n key: 'firstName',\n allowsSorting: true,\n header: () => ,\n body: user => user.first_name,\n },\n {\n key: 'lastName',\n allowsSorting: true,\n header: () => ,\n body: user => user.last_name,\n },\n {\n key: 'createdAt',\n allowsSorting: true,\n width: 'w-96',\n header: () => ,\n body: user => (\n \n ),\n },\n {\n key: 'actions',\n header: () => ,\n width: 'w-128 flex-shrink-0',\n hideHeader: true,\n align: 'end',\n visibleInMode: 'all',\n body: user => (\n
\n \n }>\n \n \n \n \n \n {user.banned_at ? (\n \n ) : (\n \n }>\n \n \n \n \n \n \n )}\n \n
\n ),\n },\n];\n\ninterface UnbanButtonProps {\n user: User;\n}\nfunction UnbanButton({user}: UnbanButtonProps) {\n const unban = useUnbanUser(user.id);\n return (\n {\n if (confirmed) {\n unban.mutate();\n }\n }}\n >\n }>\n \n \n \n \n \n }\n body={\n \n }\n confirm={}\n />\n \n );\n}\n\ninterface ImpersonateButtonProps {\n user: User;\n}\nfunction ImpersonateButton({user}: ImpersonateButtonProps) {\n const impersonate = useImpersonateUser();\n return (\n \n }>\n \n \n \n \n \n }\n isLoading={impersonate.isPending}\n body={}\n confirm={}\n onConfirm={() => {\n impersonate.mutate({userId: user.id});\n }}\n />\n \n );\n}\n","import React, {Fragment} from 'react';\nimport {Link} from 'react-router-dom';\nimport {UserDatatableFilters} from './user-datatable-filters';\nimport {DataTablePage} from '../../datatable/page/data-table-page';\nimport {Trans} from '../../i18n/trans';\nimport {DeleteSelectedItemsAction} from '../../datatable/page/delete-selected-items-action';\nimport {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';\nimport teamSvg from '../roles/team.svg';\nimport {DataTableAddItemButton} from '../../datatable/data-table-add-item-button';\nimport {DataTableExportCsvButton} from '../../datatable/csv-export/data-table-export-csv-button';\nimport {useSettings} from '../../core/settings/use-settings';\nimport {userDatatableColumns} from '@common/admin/users/user-datatable-columns';\n\nexport function UserDatatable() {\n const {billing} = useSettings();\n\n const filteredColumns = !billing.enable\n ? userDatatableColumns.filter(c => c.key !== 'subscribed')\n : userDatatableColumns;\n\n return (\n \n }\n filters={UserDatatableFilters}\n columns={filteredColumns}\n actions={}\n queryParams={{with: 'subscriptions,bans'}}\n selectedActions={}\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n \n );\n}\n\nfunction Actions() {\n return (\n \n \n \n \n \n \n );\n}\n","export function chunkArray(array: T[], chunkSize: number): T[][] {\n return array.reduce((resultArray, item, index) => {\n const chunkIndex = Math.floor(index / chunkSize);\n\n if (!resultArray[chunkIndex]) {\n resultArray[chunkIndex] = [];\n }\n\n resultArray[chunkIndex].push(item);\n\n return resultArray;\n }, []);\n}\n","import {\n IAppearanceConfig,\n MenuSectionConfig,\n} from '@common/admin/appearance/types/appearance-editor-config';\nimport {message} from '@common/i18n/message';\nimport {chunkArray} from '@common/utils/array/chunk-array';\nimport {AppearanceEditorBreadcrumbItem} from '@common/admin/appearance/types/appearance-editor-section';\n\nexport const DefaultAppearanceConfig: IAppearanceConfig = {\n preview: {\n defaultRoute: '/',\n navigationRoutes: [],\n },\n sections: {\n general: {\n label: message('General'),\n position: 1,\n buildBreadcrumb: () => [\n {\n label: message('General'),\n location: `general`,\n },\n ],\n },\n themes: {\n label: message('Themes'),\n position: 2,\n buildBreadcrumb: (pathname, formValue) => {\n const parts = pathname.split('/').filter(p => !!p);\n const [, , , themeIndex] = parts;\n const breadcrumb: AppearanceEditorBreadcrumbItem[] = [\n {\n label: message('Themes'),\n location: `themes`,\n },\n ];\n if (themeIndex != null) {\n breadcrumb.push({\n label: formValue.appearance.themes.all[+themeIndex]?.name,\n location: `themes/${themeIndex}`,\n });\n }\n if (parts.at(-1) === 'font') {\n breadcrumb.push({\n label: message('Font'),\n location: `themes/${themeIndex}/font`,\n });\n }\n if (parts.at(-1) === 'radius') {\n breadcrumb.push({\n label: message('Rounding'),\n location: `themes/${themeIndex}/radius`,\n });\n }\n return breadcrumb;\n },\n },\n menus: {\n label: message('Menus'),\n position: 3,\n buildBreadcrumb: (pathname, formValue) => {\n // /admin/appearance/menus/0/items/1\n const parts = pathname.split('/').filter(p => !!p);\n const [, , ...rest] = parts;\n // admin/appearance\n const breadcrumb: AppearanceEditorBreadcrumbItem[] = [\n {\n label: message('Menus'),\n location: 'menus',\n },\n ];\n // chunk every two items: [form group, item index]\n const chunked = chunkArray(rest, 2);\n chunked.forEach(([sectionName, sectionIndex], chunkIndex) => {\n // menu\n if (sectionName === 'menus' && sectionIndex != null) {\n breadcrumb.push({\n label: formValue.settings.menus[+sectionIndex]?.name,\n location: `menus/${sectionIndex}`,\n });\n // menu item\n } else if (sectionName === 'items' && sectionIndex != null) {\n const [, menuIndex] = chunked[chunkIndex - 1];\n breadcrumb.push({\n label:\n formValue.settings.menus[+menuIndex].items[+sectionIndex]\n ?.label,\n location: `menus/${menuIndex}/${sectionIndex}`,\n });\n }\n });\n return breadcrumb;\n },\n config: {\n availableRoutes: [\n '/',\n '/login',\n '/register',\n '/contact',\n '/pricing',\n '/account-settings',\n '/admin',\n '/admin/appearance',\n '/admin/settings',\n '/admin/plans',\n '/admin/subscriptions',\n '/admin/users',\n '/admin/roles',\n '/admin/pages',\n '/admin/tags',\n '/admin/files',\n '/admin/localizations',\n '/admin/ads',\n '/admin/settings/authentication',\n '/admin/settings/branding',\n '/admin/settings/cache',\n '/admin/settings/providers',\n '/api-docs',\n ],\n positions: [\n 'admin-navbar',\n 'admin-sidebar',\n 'custom-page-navbar',\n 'auth-page-footer',\n 'auth-dropdown',\n 'account-settings-page',\n 'billing-page',\n 'checkout-page-navbar',\n 'checkout-page-footer',\n 'pricing-table-page',\n 'contact-us-page',\n 'notifications-page',\n 'footer',\n 'footer-secondary',\n ],\n } as MenuSectionConfig,\n },\n 'custom-code': {\n label: message('Custom Code'),\n position: 4,\n buildBreadcrumb: () => [\n {\n label: message('Custom code'),\n location: `custom-code`,\n },\n ],\n },\n 'seo-settings': {\n label: message('SEO Settings'),\n position: 5,\n buildBreadcrumb: () => [\n {\n label: message('SEO'),\n location: `seo`,\n },\n ],\n },\n },\n};\n","import clsx from 'clsx';\nimport {forwardRef, ReactNode} from 'react';\nimport {KeyboardArrowRightIcon} from '../../icons/material/KeyboardArrowRight';\nimport {ButtonBase, ButtonBaseProps} from '../../ui/buttons/button-base';\n\ninterface Props extends ButtonBaseProps {\n startIcon?: ReactNode;\n description?: ReactNode;\n}\nexport const AppearanceButton = forwardRef(\n ({startIcon, children, className, description, ...other}, ref) => {\n return (\n \n {startIcon}\n \n {children}\n {description && (\n \n {description}\n \n )}\n \n \n \n );\n },\n);\n","import {createSvgIcon} from '../../../../icons/create-svg-icon';\n\nexport const ColorIcon = createSvgIcon(\n \n);\n","import React from 'react';\nimport clsx from 'clsx';\nimport {ButtonBase} from '../buttons/button-base';\n\ntype Props = {\n onChange?: (e: string) => void;\n value?: string;\n colors: string[];\n};\nexport function ColorSwatch({onChange, value, colors}: Props) {\n const presetButtons = colors.map(color => {\n const isSelected = value === color;\n return (\n {\n onChange?.(color);\n }}\n className={clsx(\n 'relative block flex-shrink-0 w-26 h-26 border rounded',\n isSelected && 'shadow-md'\n )}\n style={{backgroundColor: color}}\n >\n {isSelected && (\n \n )}\n \n );\n });\n\n return
{presetButtons}
;\n}\n","import {message} from '@common/i18n/message';\nimport {MessageDescriptor} from '@common/i18n/message-descriptor';\n\nexport const ColorPresets: {\n color: string;\n name: MessageDescriptor;\n foreground?: string;\n}[] = [\n {\n color: 'rgb(255, 255, 255)',\n name: message('White'),\n },\n {\n color: 'rgb(239,245,245)',\n name: message('Solitude'),\n },\n {\n color: 'rgb(245,213,174)',\n name: message('Wheat'),\n },\n {\n color: 'rgb(253,227,167)',\n name: message('Cape Honey'),\n },\n {\n color: 'rgb(242,222,186)',\n name: message('Milk punch'),\n },\n {\n color: 'rgb(97,118,75)',\n name: message('Dingy'),\n foreground: 'rgb(255, 255, 255)',\n },\n {\n color: 'rgb(4, 147, 114)',\n name: message('Aquamarine'),\n foreground: 'rgb(255, 255, 255)',\n },\n {\n color: 'rgb(222,245,229)',\n name: message('Cosmic Latte'),\n },\n {\n color: 'rgb(233,119,119)',\n name: message('Geraldine'),\n foreground: 'rgb(90,14,14)',\n },\n {\n color: 'rgb(247,164,164)',\n name: message('Sundown'),\n },\n {\n color: 'rgb(30,139,195)',\n name: message('Pelorous'),\n foreground: 'rgb(255, 255, 255)',\n },\n {\n color: 'rgb(142,68,173)',\n name: message('Deep Lilac'),\n foreground: 'rgb(255, 255, 255)',\n },\n {\n color: 'rgb(108,74,182)',\n name: message('Blue marguerite'),\n foreground: 'rgb(255, 255, 255)',\n },\n {\n color: 'rgb(139,126,116)',\n name: message('Americano'),\n foreground: 'rgb(255, 255, 255)',\n },\n {\n color: 'rgb(0,0,0)',\n name: message('Black'),\n foreground: 'rgb(255, 255, 255)',\n },\n {\n color: 'rgb(64,66,88)',\n name: message('Blue zodiac'),\n foreground: 'rgb(255, 255, 255)',\n },\n {\n color: 'rgb(101,100,124)',\n name: message('Comet'),\n foreground: 'rgb(255, 255, 255)',\n },\n];\n","import {HexColorInput, HexColorPicker} from 'react-colorful';\nimport React, {useState} from 'react';\nimport {parseColor} from '@react-stately/color';\nimport {ColorSwatch} from './color-swatch';\nimport {getInputFieldClassNames} from '../forms/input-field/get-input-field-class-names';\nimport {ColorPresets} from '@common/ui/color-picker/color-presets';\n\nconst DefaultPresets = ColorPresets.map(({color}) => color).slice(0, 14);\n\ntype Props = {\n defaultValue?: string;\n onChange?: (e: string) => void;\n colorPresets?: string[];\n showInput?: boolean;\n};\nexport function ColorPicker({\n defaultValue,\n onChange,\n colorPresets,\n showInput,\n}: Props) {\n const [color, setColor] = useState(defaultValue);\n\n const presets: string[] = colorPresets || DefaultPresets;\n\n const style = getInputFieldClassNames({size: 'sm'});\n\n return (\n
\n {\n onChange?.(newColor);\n setColor(newColor);\n }}\n />\n
\n {presets && (\n {\n if (newColor) {\n const hex = parseColor(newColor).toString('hex');\n onChange?.(hex);\n setColor(hex);\n }\n }}\n value={color}\n />\n )}\n {showInput && (\n
\n {\n onChange?.(newColor);\n setColor(newColor);\n }}\n />\n
\n )}\n
\n
\n );\n}\n","import {ColorPicker} from './color-picker';\nimport {DialogFooter} from '../overlays/dialog/dialog-footer';\nimport {Button} from '../buttons/button';\nimport {useDialogContext} from '../overlays/dialog/dialog-context';\nimport {Dialog} from '../overlays/dialog/dialog';\nimport {Trans} from '../../i18n/trans';\n\ninterface ColorPickerDialogProps {\n hideFooter?: boolean;\n showInput?: boolean;\n}\nexport function ColorPickerDialog({\n hideFooter = false,\n showInput = true,\n}: ColorPickerDialogProps) {\n const {close, value, setValue, initialValue} = useDialogContext<\n string | null\n >();\n // todo: remove this once pixie and bedrive are refactored to use dialogTrigger currentValue (use \"currentValue\" for defaultValue as well)\n //const initialValue = useRef(defaultValue);\n\n return (\n \n setValue(newValue)}\n />\n {!hideFooter && (\n \n \n close(value)}\n >\n \n \n \n )}\n \n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {\n appearanceState,\n AppearanceValues,\n useAppearanceStore,\n} from '@common/admin/appearance/appearance-store';\nimport {Fragment, ReactNode} from 'react';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {Trans} from '@common/i18n/trans';\nimport {FormImageSelector} from '@common/ui/images/image-selector';\nimport {FormSlider} from '@common/ui/forms/slider/slider';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {AppearanceButton} from '@common/admin/appearance/appearance-button';\nimport {ColorIcon} from '@common/admin/appearance/sections/themes/color-icon';\nimport {ColorPickerDialog} from '@common/ui/color-picker/color-picker-dialog';\nimport {Link} from 'react-router-dom';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {LandingPageContent} from '@app/landing-page/landing-page-content';\n\nexport function LandingPageSectionGeneral() {\n return (\n \n \n
\n \n \n \n \n \n \n \n \n \n
\n \n \n
\n );\n}\n\nfunction HeaderSection() {\n const defaultImage = useAppearanceStore(\n s => s.defaults?.settings.homepage?.appearance?.headerImage,\n );\n\n return (\n \n }\n className=\"mb-20\"\n name=\"settings.homepage.appearance.headerTitle\"\n onFocus={() => {\n appearanceState().preview.setHighlight('[data-testid=\"headerTitle\"]');\n }}\n />\n }\n className=\"mb-30\"\n inputElementType=\"textarea\"\n rows={4}\n name=\"settings.homepage.appearance.headerSubtitle\"\n onFocus={() => {\n appearanceState().preview.setHighlight(\n '[data-testid=\"headerSubtitle\"]',\n );\n }}\n />\n }\n defaultValue={defaultImage}\n diskPrefix=\"homepage\"\n />\n \n \n \n }\n minValue={0}\n step={0.1}\n maxValue={1}\n formatOptions={{style: 'percent'}}\n />\n
\n \n
\n }\n />\n }\n />\n
\n );\n}\n\nfunction FooterSection() {\n const defaultImage = useAppearanceStore(\n s =>\n (s.defaults?.settings.homepage?.appearance as LandingPageContent)\n ?.footerImage,\n );\n return (\n \n \n \n \n }\n className=\"mb-20\"\n name=\"settings.homepage.appearance.footerTitle\"\n onFocus={() => {\n appearanceState().preview.setHighlight('[data-testid=\"footerTitle\"]');\n }}\n />\n }\n className=\"mb-20\"\n name=\"settings.homepage.appearance.footerSubtitle\"\n onFocus={() => {\n appearanceState().preview.setHighlight(\n '[data-testid=\"footerSubtitle\"]',\n );\n }}\n />\n }\n defaultValue={defaultImage}\n diskPrefix=\"homepage\"\n />\n \n );\n}\n\nfunction PricingSection() {\n return (\n
\n }\n className=\"mb-20\"\n name=\"settings.homepage.appearance.pricingTitle\"\n onFocus={() => {\n appearanceState().preview.setHighlight(\n '[data-testid=\"pricingTitle\"]',\n );\n }}\n />\n }\n className=\"mb-20\"\n name=\"settings.homepage.appearance.pricingSubtitle\"\n onFocus={() => {\n appearanceState().preview.setHighlight(\n '[data-testid=\"pricingSubtitle\"]',\n );\n }}\n />\n \n \n \n
\n );\n}\n\ninterface ColorPickerTriggerProps {\n formKey: string;\n label: ReactNode;\n}\nfunction ColorPickerTrigger({label, formKey}: ColorPickerTriggerProps) {\n const key = formKey as 'settings.homepage.appearance.headerOverlayColor1';\n const {watch, setValue} = useFormContext();\n\n const formValue = watch(key);\n\n const setColor = (value: string | null) => {\n setValue(formKey as any, value, {\n shouldDirty: true,\n });\n };\n\n return (\n setColor(newValue)}\n type=\"popover\"\n onClose={value => setColor(value)}\n >\n \n }\n >\n {label}\n \n \n \n );\n}\n","export function ucFirst(string: T): T {\n if (!string) return string;\n return (string.charAt(0).toUpperCase() + string.slice(1)) as T;\n}\n","import {useControlledState} from '@react-stately/utils';\nimport React, {Fragment, useState} from 'react';\nimport {useController} from 'react-hook-form';\nimport {mergeProps} from '@react-aria/utils';\nimport clsx from 'clsx';\nimport {produce} from 'immer';\nimport {Permission, PermissionRestriction} from '../permission';\nimport {useValueLists} from '../../http/value-lists';\nimport {ucFirst} from '../../utils/string/uc-first';\nimport {Accordion, AccordionItem} from '../../ui/accordion/accordion';\nimport {List, ListItem} from '../../ui/list/list';\nimport {Switch} from '../../ui/forms/toggle/switch';\nimport {TextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {DoneAllIcon} from '../../icons/material/DoneAll';\nimport {Trans} from '../../i18n/trans';\n\ninterface PermissionSelectorProps {\n value?: Permission[];\n onChange?: (value: Permission[]) => void;\n valueListKey?: 'permissions' | 'workspacePermissions';\n}\nexport const PermissionSelector = React.forwardRef<\n HTMLDivElement,\n PermissionSelectorProps\n>(({valueListKey = 'permissions', ...props}, ref) => {\n const {data} = useValueLists([valueListKey]);\n const permissions = data?.permissions || data?.workspacePermissions;\n\n const [value, setValue] = useControlledState(props.value, [], props.onChange);\n const [showAdvanced, setShowAdvanced] = useState(false);\n\n if (!permissions) return null;\n\n const groupedPermissions = buildPermissionList(\n permissions,\n value,\n showAdvanced\n );\n\n const onRestrictionChange = (newPermission: Permission) => {\n const newValue = [...value];\n const index = newValue.findIndex(p => p.id === newPermission.id);\n if (index > -1) {\n newValue.splice(index, 1, newPermission);\n }\n setValue(newValue);\n };\n\n return (\n \n \n {groupedPermissions.map(({groupName, items, anyChecked}) => (\n }\n key={groupName}\n startIcon={anyChecked ? : undefined}\n >\n \n {items.map(permission => {\n const index = value.findIndex(v => v.id === permission.id);\n const isChecked = index > -1;\n\n return (\n
\n {\n if (isChecked) {\n const newValue = [...value];\n newValue.splice(index, 1);\n setValue(newValue);\n } else {\n setValue([...value, permission]);\n }\n }}\n endSection={\n {}}\n />\n }\n description={}\n >\n \n \n {isChecked && (\n \n )}\n
\n );\n })}\n
\n \n ))}\n
\n {\n setShowAdvanced(e.target.checked);\n }}\n >\n \n \n
\n );\n});\n\ninterface RestrictionsProps {\n permission: Permission;\n onChange?: (newPermission: Permission) => void;\n}\nfunction Restrictions({permission, onChange}: RestrictionsProps) {\n if (!permission?.restrictions?.length) return null;\n\n const setRestrictionValue = (\n name: string,\n value: PermissionRestriction['value']\n ) => {\n const nextState = produce(permission, draftState => {\n const restriction = draftState.restrictions.find(r => r.name === name);\n if (restriction) {\n restriction.value = value;\n }\n });\n onChange?.(nextState);\n };\n\n return (\n
\n {permission.restrictions.map((restriction, index) => {\n const isLast = index === permission.restrictions.length - 1;\n\n const name = ;\n const description = restriction.description ? (\n \n ) : undefined;\n\n if (restriction.type === 'bool') {\n return (\n {\n setRestrictionValue(restriction.name, e.target.checked);\n }}\n >\n {name}\n \n );\n }\n\n return (\n {\n setRestrictionValue(\n restriction.name,\n e.target.value === '' ? undefined : parseInt(e.target.value)\n );\n }}\n />\n );\n })}\n
\n );\n}\n\nexport type FormChipFieldProps = PermissionSelectorProps & {\n name: string;\n};\nexport function FormPermissionSelector(props: FormChipFieldProps) {\n const {\n field: {onChange, value = [], ref},\n } = useController({\n name: props.name,\n });\n\n const formProps: Partial = {\n onChange,\n value,\n };\n\n return ;\n}\n\nexport const prettyName = (name: string) => {\n return ucFirst(name.replace('_', ' '));\n};\n\ninterface PermissionGroup {\n groupName: string;\n anyChecked: boolean;\n items: Permission[];\n}\n\n// merge \"restrictions\" from selected value into all permissions to make\n// it easier to bind restriction values to form inputs\nexport function buildPermissionList(\n allPermissions: Permission[],\n selectedPermissions: Permission[],\n showAdvanced: boolean\n) {\n const groupedPermissions: PermissionGroup[] = [];\n\n allPermissions.forEach(permission => {\n const index = selectedPermissions.findIndex(p => p.id === permission.id);\n if (!showAdvanced && permission.advanced) return;\n\n let group: PermissionGroup | undefined = groupedPermissions.find(\n g => g.groupName === permission.group\n );\n if (!group) {\n group = {groupName: permission.group, anyChecked: false, items: []};\n groupedPermissions.push(group);\n }\n\n if (index > -1) {\n const mergedPermission = {\n ...permission,\n restrictions: mergeRestrictions(\n permission.restrictions,\n selectedPermissions[index].restrictions\n ),\n };\n group.anyChecked = true;\n group.items.push(mergedPermission);\n } else {\n group.items.push(permission);\n }\n });\n\n return groupedPermissions;\n}\n\nfunction mergeRestrictions(\n allRestrictions: PermissionRestriction[],\n selectedRestrictions: PermissionRestriction[]\n): PermissionRestriction[] {\n return allRestrictions?.map(restriction => {\n const selected = selectedRestrictions.find(\n r => r.name === restriction.name\n );\n if (selected) {\n return {...restriction, value: selected.value};\n } else {\n return restriction;\n }\n });\n}\n","import {MenuSectionConfig} from '../../../types/appearance-editor-config';\nimport {MenuItemConfig} from '../../../../../core/settings/settings';\nimport mergedAppearanceConfig from '../../../config/merged-appearance-config';\n\nexport function useAvailableRoutes(): Partial[] {\n const menuConfig = mergedAppearanceConfig.sections.menus.config;\n\n if (!menuConfig) return [];\n\n return (menuConfig as MenuSectionConfig).availableRoutes.map(route => {\n return {\n id: route,\n label: route,\n action: route,\n type: 'route',\n target: '_self',\n };\n });\n}\n","export const iconGridStyle = {\n grid: 'flex flex-wrap gap-24',\n button:\n 'flex flex-col items-center rounded hover:bg-hover h-90 aspect-square',\n};\n","import React, {Suspense} from 'react';\nimport {IconTree} from '../../icons/create-svg-icon';\nimport {iconGridStyle} from './icon-grid-style';\nimport {TextField} from '../forms/input-field/text-field/text-field';\nimport {Skeleton} from '../skeleton/skeleton';\nimport {useTrans} from '../../i18n/use-trans';\nimport {AnimatePresence, m} from 'framer-motion';\nimport {opacityAnimation} from '../animation/opacity-animation';\n\nconst skeletons = [...Array(60).keys()];\n\nconst IconList = React.lazy(() => import('./icon-list'));\n\ninterface IconListProps {\n onIconSelected: (icon: IconTree[] | null) => void;\n}\nexport default function IconPicker({onIconSelected}: IconListProps) {\n const {trans} = useTrans();\n const [value, setValue] = React.useState('');\n\n return (\n
\n {\n setValue(e.target.value);\n }}\n placeholder={trans({message: 'Search icons...'})}\n />\n \n \n {skeletons.map((_, index) => (\n
\n \n
\n ))}\n \n }\n >\n \n \n \n \n
\n
\n );\n}\n","import React from 'react';\nimport IconPicker from './icon-picker';\nimport {useDialogContext} from '../overlays/dialog/dialog-context';\nimport {Dialog} from '../overlays/dialog/dialog';\nimport {DialogHeader} from '../overlays/dialog/dialog-header';\nimport {DialogBody} from '../overlays/dialog/dialog-body';\nimport {Trans} from '../../i18n/trans';\n\nexport function IconPickerDialog() {\n return (\n \n \n \n \n \n \n \n \n );\n}\n\nfunction IconPickerWrapper() {\n const {close} = useDialogContext();\n return (\n {\n close(value);\n }}\n />\n );\n}\n","import {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../i18n/trans';\nimport {useValueLists} from '../../http/value-lists';\nimport {useTrans} from '../../i18n/use-trans';\nimport {FormChipField} from '../../ui/forms/input-field/chip-field/form-chip-field';\nimport {Item} from '../../ui/forms/listbox/item';\nimport {Fragment, useEffect, useMemo} from 'react';\nimport {\n buildPermissionList,\n prettyName,\n} from '../../auth/ui/permission-selector';\nimport {Section} from '../../ui/forms/listbox/section';\nimport {useFormContext} from 'react-hook-form';\nimport {MenuItemConfig} from '../../core/settings/settings';\nimport {FormSelect, Option} from '../../ui/forms/select/select';\nimport {useAvailableRoutes} from '../appearance/sections/menus/hooks/available-routes';\nimport {ButtonBaseProps} from '../../ui/buttons/button-base';\nimport {createSvgIconFromTree, IconTree} from '../../icons/create-svg-icon';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {EditIcon} from '../../icons/material/Edit';\nimport {IconPickerDialog} from '../../ui/icon-picker/icon-picker-dialog';\nimport {message} from '../../i18n/message';\nimport {usePrevious} from '../../utils/hooks/use-previous';\n\ninterface NameProps {\n prefixName: (name: string) => string;\n}\n\ninterface MenuItemFormProps {\n formPathPrefix?: string;\n hideRoleAndPermissionFields?: boolean;\n}\nexport function MenuItemForm({\n formPathPrefix,\n hideRoleAndPermissionFields,\n}: MenuItemFormProps) {\n const {trans} = useTrans();\n const prefixName = (name: string): string => {\n return formPathPrefix ? `${formPathPrefix}.${name}` : name;\n };\n\n return (\n \n }\n placeholder={trans(message('No label...'))}\n startAppend={}\n />\n \n {!hideRoleAndPermissionFields && (\n \n \n \n \n )}\n \n \n );\n}\n\ninterface IconDialogTriggerProps extends ButtonBaseProps, NameProps {}\nfunction IconDialogTrigger({\n prefixName,\n ...buttonProps\n}: IconDialogTriggerProps) {\n const {watch, setValue} = useFormContext();\n const fieldName = prefixName('icon') as 'icon';\n const watchedItemIcon = watch(fieldName);\n const Icon = watchedItemIcon && createSvgIconFromTree(watchedItemIcon);\n return (\n {\n // null will be set explicitly if icon is cleared via icon picker\n if (iconTree || iconTree === null) {\n setValue(fieldName, iconTree, {\n shouldDirty: true,\n });\n }\n }}\n >\n \n {Icon ? : }\n \n \n \n );\n}\n\nfunction DestinationSelector({prefixName}: NameProps) {\n const form = useFormContext();\n const currentType = form.watch(prefixName('type') as 'type');\n const previousType = usePrevious(currentType);\n const {data} = useValueLists(['menuItemCategories']);\n const categories = data?.menuItemCategories || [];\n const selectedCategory = categories.find(c => c.type === currentType);\n const {trans} = useTrans();\n const routeItems = useAvailableRoutes();\n\n // clear \"action\" field when \"type\" field changes\n useEffect(() => {\n if (previousType && previousType !== currentType) {\n form.setValue(prefixName('action') as 'action', '');\n }\n }, [currentType, previousType, form, prefixName]);\n\n return (\n \n }\n >\n \n \n {categories.map(category => (\n \n ))}\n \n {currentType === 'link' && (\n }\n />\n )}\n {currentType === 'route' && (\n }\n searchPlaceholder={trans(message('Search pages'))}\n showSearchField\n selectionMode=\"single\"\n >\n {item => (\n \n {item.label}\n \n )}\n \n )}\n {selectedCategory && (\n }\n >\n {item => (\n \n \n \n )}\n \n )}\n \n );\n}\n\nfunction RoleSelector({prefixName}: NameProps) {\n const {data} = useValueLists(['roles', 'permissions']);\n const roles = data?.roles || [];\n const {trans} = useTrans();\n\n return (\n }\n name={prefixName('roles')}\n chipSize=\"sm\"\n suggestions={roles}\n valueKey=\"id\"\n displayWith={c => roles.find(r => r.id === c.id)?.name}\n >\n {role => (\n \n \n \n )}\n \n );\n}\n\nfunction PermissionSelector({prefixName}: NameProps) {\n const {data} = useValueLists(['roles', 'permissions']);\n const {trans} = useTrans();\n\n const groupedPermissions = useMemo(() => {\n return buildPermissionList(data?.permissions || [], [], false);\n }, [data?.permissions]);\n\n return (\n }\n placeholder={trans({message: 'Add permission...'})}\n chipSize=\"sm\"\n suggestions={groupedPermissions}\n name={prefixName('permissions')}\n valueKey=\"name\"\n >\n {({groupName, items}) => (\n
\n {items.map(permission => (\n }\n >\n \n \n ))}\n
\n )}\n \n );\n}\n\nfunction TargetSelect({prefixName}: NameProps) {\n return (\n }\n >\n \n \n \n );\n}\n","import {MenuItemForm} from '@common/admin/menus/menu-item-form';\nimport {Accordion, AccordionItem} from '@common/ui/accordion/accordion';\nimport {Trans} from '@common/i18n/trans';\nimport {appearanceState} from '@common/admin/appearance/appearance-store';\nimport {useState} from 'react';\n\nexport function LandingPageSectionActionButtons() {\n const [expandedValues, setExpandedValues] = useState(['cta1']);\n return (\n {\n setExpandedValues(values as string[]);\n if (values.length) {\n appearanceState().preview.setHighlight(\n `[data-testid=\"${values[0]}\"]`\n );\n }\n }}\n >\n }>\n \n \n }>\n \n \n }>\n \n \n \n );\n}\n","import {Accordion, AccordionItem} from '@common/ui/accordion/accordion';\nimport {Trans} from '@common/i18n/trans';\nimport {\n appearanceState,\n useAppearanceStore,\n} from '@common/admin/appearance/appearance-store';\nimport {useFieldArray} from 'react-hook-form';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {FormImageSelector} from '@common/ui/images/image-selector';\nimport {Button} from '@common/ui/buttons/button';\nimport {AddIcon} from '@common/icons/material/Add';\nimport {useState} from 'react';\n\nexport function LandingPageSectionPrimaryFeatures() {\n const {fields, remove, append} = useFieldArray({\n name: 'settings.homepage.appearance.primaryFeatures',\n });\n const [expandedValues, setExpandedValues] = useState([0]);\n return (\n
\n {\n setExpandedValues(values as number[]);\n if (values.length) {\n appearanceState().preview.setHighlight(\n `[data-testid=\"primary-root-${values[0]}\"]`\n );\n }\n }}\n >\n {fields.map((field, index) => {\n return (\n }\n >\n \n
\n {\n remove(index);\n }}\n >\n \n \n
\n \n );\n })}\n \n
\n }\n onClick={() => {\n append({});\n setExpandedValues([fields.length]);\n }}\n >\n \n \n
\n
\n );\n}\n\ninterface FeatureFormProps {\n index: number;\n}\nfunction FeatureForm({index}: FeatureFormProps) {\n const defaultImage = useAppearanceStore(\n s =>\n s.defaults?.settings.homepage?.appearance?.primaryFeatures?.[index]?.image\n );\n\n return (\n <>\n }\n defaultValue={defaultImage}\n diskPrefix=\"homepage\"\n />\n }\n className=\"mb-20\"\n onFocus={() => {\n appearanceState().preview.setHighlight(\n `[data-testid=\"primary-title-${index}\"]`\n );\n }}\n />\n }\n className=\"mb-20\"\n inputElementType=\"textarea\"\n rows={4}\n onFocus={() => {\n appearanceState().preview.setHighlight(\n `[data-testid=\"primary-subtitle-${index}\"]`\n );\n }}\n />\n \n );\n}\n","import {Accordion, AccordionItem} from '@common/ui/accordion/accordion';\nimport {Trans} from '@common/i18n/trans';\nimport {appearanceState} from '@common/admin/appearance/appearance-store';\nimport {useFieldArray} from 'react-hook-form';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {FormImageSelector} from '@common/ui/images/image-selector';\nimport {Button} from '@common/ui/buttons/button';\nimport {AddIcon} from '@common/icons/material/Add';\nimport {useState} from 'react';\n\nexport function LandingPageSecondaryFeatures() {\n const {fields, remove, append} = useFieldArray({\n name: 'settings.homepage.appearance.secondaryFeatures',\n });\n const [expandedValues, setExpandedValues] = useState([0]);\n return (\n
\n {\n setExpandedValues(values as number[]);\n if (values.length) {\n appearanceState().preview.setHighlight(\n `[data-testid=\"secondary-root-${values[0]}\"]`\n );\n }\n }}\n >\n {fields.map((field, index) => {\n return (\n }\n >\n \n
\n {\n remove(index);\n }}\n >\n \n \n
\n \n );\n })}\n \n
\n }\n onClick={() => {\n append({});\n setExpandedValues([fields.length]);\n }}\n >\n \n \n
\n
\n );\n}\n\ninterface FeatureFormProps {\n index: number;\n}\nfunction FeatureForm({index}: FeatureFormProps) {\n return (\n <>\n }\n defaultValue={getDefaultImage(index)}\n diskPrefix=\"homepage\"\n />\n }\n className=\"mb-20\"\n onFocus={() => {\n appearanceState().preview.setHighlight(\n `[data-testid=\"secondary-title-${index}\"]`\n );\n }}\n />\n }\n className=\"mb-20\"\n inputElementType=\"textarea\"\n rows={4}\n onFocus={() => {\n appearanceState().preview.setHighlight(\n `[data-testid=\"secondary-subtitle-${index}\"]`\n );\n }}\n />\n }\n className=\"mb-20\"\n inputElementType=\"textarea\"\n rows={4}\n onFocus={() => {\n appearanceState().preview.setHighlight(\n `[data-testid=\"secondary-description-${index}\"]`\n );\n }}\n />\n \n );\n}\n\nfunction getDefaultImage(index: number): string | undefined {\n return appearanceState().defaults?.settings.homepage?.appearance\n .secondaryFeatures[index]?.image;\n}\n","import {\n IAppearanceConfig,\n MenuSectionConfig,\n SeoSettingsSectionConfig,\n} from '@common/admin/appearance/types/appearance-editor-config';\nimport {message} from '@common/i18n/message';\nimport {LandingPageSectionGeneral} from '@app/admin/appearance/sections/landing-page-section/landing-page-section-general';\nimport {LandingPageSectionActionButtons} from '@app/admin/appearance/sections/landing-page-section/landing-page-section-action-buttons';\nimport {LandingPageSectionPrimaryFeatures} from '@app/admin/appearance/sections/landing-page-section/landing-page-section-primary-features';\nimport {LandingPageSecondaryFeatures} from '@app/admin/appearance/sections/landing-page-section/landing-page-section-secondary-features';\nimport {AppearanceEditorBreadcrumbItem} from '@common/admin/appearance/types/appearance-editor-section';\n\nexport const AppAppearanceConfig: IAppearanceConfig = {\n preview: {\n defaultRoute: 'dashboard',\n navigationRoutes: ['dashboard'],\n },\n sections: {\n 'landing-page': {\n label: message('Landing Page'),\n position: 1,\n previewRoute: '/',\n routes: [\n {path: 'landing-page', element: },\n {\n path: 'landing-page/action-buttons',\n element: ,\n },\n {\n path: 'landing-page/primary-features',\n element: ,\n },\n {\n path: 'landing-page/secondary-features',\n element: ,\n },\n ],\n buildBreadcrumb: pathname => {\n const parts = pathname.split('/').filter(p => !!p);\n const sectionName = parts.pop();\n // admin/appearance\n const breadcrumb: AppearanceEditorBreadcrumbItem[] = [\n {\n label: message('Landing page'),\n location: 'landing-page',\n },\n ];\n if (sectionName === 'action-buttons') {\n breadcrumb.push({\n label: message('Action buttons'),\n location: 'landing-page/action-buttons',\n });\n }\n\n if (sectionName === 'primary-features') {\n breadcrumb.push({\n label: message('Primary features'),\n location: 'landing-page/primary-features',\n });\n }\n\n if (sectionName === 'secondary-features') {\n breadcrumb.push({\n label: message('Secondary features'),\n location: 'landing-page/secondary-features',\n });\n }\n\n return breadcrumb;\n },\n },\n // missing label will get added by deepMerge from default config\n // @ts-ignore\n menus: {\n config: {\n positions: [\n 'sidebar-primary',\n 'sidebar-secondary',\n 'mobile-bottom',\n 'landing-page-navbar',\n 'landing-page-footer',\n ],\n availableRoutes: [\n '/lists',\n '/watchlist',\n '/admin/channels',\n '/admin/comments',\n ],\n } as MenuSectionConfig,\n },\n // @ts-ignore\n 'seo-settings': {\n config: {\n pages: [\n {\n key: 'title-page',\n label: message('Title page'),\n },\n {\n key: 'season-page',\n label: message('Season page'),\n },\n {\n key: 'episode-page',\n label: message('Episode page'),\n },\n {\n key: 'watch-page',\n label: message('Watch page'),\n },\n {\n key: 'person-page',\n label: message('Person page'),\n },\n {\n key: 'landing-page',\n label: message('Landing page'),\n },\n {\n key: 'news-article-page',\n label: message('News article page'),\n },\n {\n key: 'channel-page',\n label: message('Channel page'),\n },\n ],\n } as SeoSettingsSectionConfig,\n },\n },\n};\n","import deepMerge from 'deepmerge';\nimport {DefaultAppearanceConfig} from '@common/admin/appearance/config/default-appearance-config';\nimport {AppAppearanceConfig} from '@app/admin/appearance/app-appearance-config';\nimport {IAppearanceConfig} from '@common/admin/appearance/types/appearance-editor-config';\n\nconst mergedAppearanceConfig = deepMerge.all([\n DefaultAppearanceConfig,\n AppAppearanceConfig,\n]);\n\nexport default mergedAppearanceConfig as IAppearanceConfig;\n","import {create} from 'zustand';\nimport {subscribeWithSelector} from 'zustand/middleware';\nimport {immer} from 'zustand/middleware/immer';\nimport {Settings} from '../../core/settings/settings';\nimport type {IAppearanceConfig} from './types/appearance-editor-config';\nimport {AllCommands} from './commands/commands';\nimport mergedAppearanceConfig from './config/merged-appearance-config';\nimport {BootstrapData} from '../../core/bootstrap-data/bootstrap-data';\nimport {FontConfig} from '@common/http/value-lists';\n\nexport interface AppearanceValues {\n appearance: {\n env: {app_name: string; app_url: string};\n seo: {\n key: string;\n name: string;\n value: string;\n defaultValue: string;\n }[];\n themes: BootstrapData['themes'];\n custom_code: {\n css?: string;\n html?: string;\n };\n };\n settings: Settings;\n}\n\nexport interface AppearanceDefaults {\n appearance: {\n themes: {\n light: Record;\n dark: Record;\n };\n };\n settings: Settings;\n}\n\ninterface AppearanceStore {\n defaults: AppearanceDefaults | null;\n iframeWindow: Window | null;\n config: IAppearanceConfig | null;\n setDefaults: (value: AppearanceDefaults) => void;\n setIframeWindow: (value: Window) => void;\n preview: {\n navigate: (sectionName: string) => void;\n setValues: (settings: AppearanceValues) => void;\n setThemeFont: (font: FontConfig | null) => void;\n setThemeValue: (name: string, value: string) => void;\n setActiveTheme: (themeId: number | string) => void;\n setHighlight: (selector: string | null | undefined) => void;\n setCustomCode: (mode: 'css' | 'html', value?: string) => void;\n };\n}\n\nexport const useAppearanceStore = create()(\n subscribeWithSelector(\n immer((set, get) => ({\n defaults: null,\n iframeWindow: null,\n config: mergedAppearanceConfig,\n setDefaults: value => {\n set(state => {\n state.defaults = {...value};\n });\n },\n setIframeWindow: value => {\n set(() => {\n return {iframeWindow: value};\n });\n },\n\n preview: {\n navigate: sectionName => {\n const section = get().config?.sections[sectionName];\n const route = section?.previewRoute || '/';\n const preview = get().iframeWindow;\n if (route) {\n postMessage(preview, {type: 'navigate', to: route});\n }\n },\n setValues: values => {\n const preview = get().iframeWindow;\n postMessage(preview, {type: 'setValues', values});\n },\n setThemeFont: font => {\n const preview = get().iframeWindow;\n postMessage(preview, {type: 'setThemeFont', value: font});\n },\n setThemeValue: (name, value) => {\n const preview = get().iframeWindow;\n postMessage(preview, {type: 'setThemeValue', name, value});\n },\n setActiveTheme: themeId => {\n const preview = get().iframeWindow;\n postMessage(preview, {type: 'setActiveTheme', themeId});\n },\n setCustomCode: (mode, value) => {\n const preview = get().iframeWindow;\n postMessage(preview, {type: 'setCustomCode', mode, value});\n },\n setHighlight: selector => {\n set(() => {\n let node: HTMLElement | null = null;\n const document = get().iframeWindow?.document;\n if (document && selector) {\n node = document.querySelector(selector);\n }\n if (node) {\n requestAnimationFrame(() => {\n if (!node) return;\n node.scrollIntoView({\n behavior: 'smooth',\n block: 'center',\n inline: 'center',\n });\n });\n }\n });\n },\n },\n })),\n ),\n);\n\nfunction postMessage(window: Window | null, command: AllCommands) {\n if (window) {\n window.postMessage({source: 'be-appearance-editor', ...command}, '*');\n }\n}\n\nexport function appearanceState() {\n return useAppearanceStore.getState();\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {AppearanceValues} from '@common/admin/appearance/appearance-store';\nimport {toast} from '@common/ui/toast/toast';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\nimport {message} from '@common/i18n/message';\n\ninterface Response extends BackendResponse {}\n\nexport function useSaveAppearanceChanges() {\n return useMutation({\n mutationFn: (values: Partial) =>\n saveAppearanceChanges(values),\n onSuccess: async () => {\n await queryClient.invalidateQueries({\n queryKey: ['admin/appearance/values'],\n });\n toast(message('Changes saved'));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction saveAppearanceChanges(\n changes: Partial,\n): Promise {\n return apiClient.post(`admin/appearance`, {changes}).then(r => r.data);\n}\n","import {useQuery} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\nimport {AppearanceDefaults, AppearanceValues} from '../appearance-store';\n\nexport interface FetchAppearanceValuesResponse extends BackendResponse {\n values: AppearanceValues;\n defaults: AppearanceDefaults;\n}\n\nexport function useAppearanceValues() {\n return useQuery({\n queryKey: ['admin/appearance/values'],\n queryFn: () => fetchAppearanceValues(),\n staleTime: Infinity,\n });\n}\n\nfunction fetchAppearanceValues(): Promise {\n return apiClient\n .get('admin/appearance/values')\n .then(response => response.data);\n}\n","import {Link, useLocation} from 'react-router-dom';\nimport clsx from 'clsx';\nimport {Fragment, useEffect, useState} from 'react';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {KeyboardArrowLeftIcon} from '../../icons/material/KeyboardArrowLeft';\nimport {KeyboardArrowRightIcon} from '../../icons/material/KeyboardArrowRight';\nimport {Trans} from '../../i18n/trans';\nimport {MixedText} from '../../i18n/mixed-text';\nimport {useFormContext} from 'react-hook-form';\nimport {appearanceState, AppearanceValues} from './appearance-store';\nimport {AppearanceEditorBreadcrumbItem} from './types/appearance-editor-section';\nimport {message} from '../../i18n/message';\n\nexport function SectionHeader() {\n const {pathname} = useLocation();\n const {getValues} = useFormContext();\n const [breadcrumb, setBreadcrumb] = useState<\n AppearanceEditorBreadcrumbItem[] | null\n >(null);\n\n useEffect(() => {\n const [, , sectionName] = pathname.split('/').filter(p => !!p);\n if (sectionName) {\n const section = appearanceState().config?.sections[sectionName];\n if (section) {\n setBreadcrumb([\n {\n label: message('Appearance'),\n location: '',\n },\n ...section.buildBreadcrumb(pathname, getValues()),\n ]);\n // bail, so breadcrumb is not cleared below\n return;\n }\n }\n setBreadcrumb(null);\n }, [pathname, getValues]);\n\n // not need to show section header if already at root\n if (!breadcrumb || breadcrumb.length < 2) {\n return null;\n }\n\n return (\n
\n \n \n \n
\n
\n \n
\n
\n {breadcrumb.map((item, index) => {\n const isLast = breadcrumb.length - 1 === index;\n const isFirst = index === 0;\n const label = ;\n\n if (isFirst) {\n return null;\n }\n\n return (\n \n \n {label}\n
\n {!isLast && (\n \n )}\n \n );\n })}\n
\n
\n \n );\n}\n","import {Link, Navigate, Outlet, useLocation} from 'react-router-dom';\nimport {useEffect, useRef} from 'react';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {CloseIcon} from '../../icons/material/Close';\nimport {Button} from '../../ui/buttons/button';\nimport {appearanceState, AppearanceValues} from './appearance-store';\nimport {useSaveAppearanceChanges} from './requests/save-appearance-changes';\nimport {useAppearanceValues} from './requests/appearance-values';\nimport {Trans} from '../../i18n/trans';\nimport {useForm, useFormContext} from 'react-hook-form';\nimport {Form} from '../../ui/forms/form';\nimport {ProgressCircle} from '../../ui/progress/progress-circle';\nimport {SectionHeader} from './section-header';\nimport {FileUploadProvider} from '../../uploads/uploader/file-upload-provider';\nimport {useAppearanceEditorMode} from './commands/use-appearance-editor-mode';\nimport {StaticPageTitle} from '../../seo/static-page-title';\nimport {useSettings} from '../../core/settings/use-settings';\n\nexport function AppearanceLayout() {\n const {isAppearanceEditorActive} = useAppearanceEditorMode();\n const {data} = useAppearanceValues();\n const {base_url} = useSettings();\n const iframeRef = useRef(null);\n const {pathname} = useLocation();\n\n useEffect(() => {\n // only set defaults snapshot once on route init\n if (data?.defaults && !appearanceState().defaults) {\n appearanceState().setDefaults(data.defaults);\n }\n }, [data]);\n\n useEffect(() => {\n if (iframeRef.current) {\n appearanceState().setIframeWindow(iframeRef.current.contentWindow!);\n }\n }, []);\n\n useEffect(() => {\n const sectionName = pathname.split('/')[3];\n appearanceState().preview.navigate(sectionName);\n }, [pathname]);\n\n // make sure appearance editor iframe can't be nested\n if (isAppearanceEditorActive) {\n return ;\n }\n\n return (\n
\n \n \n \n \n
\n \n
\n
\n );\n}\n\ninterface SidebarProps {\n values: AppearanceValues | undefined;\n}\nfunction Sidebar({values}: SidebarProps) {\n const spinner = (\n
\n \n
\n );\n\n return (\n
\n {values ? : spinner}\n
\n );\n}\n\ninterface AppearanceFormProps {\n defaultValues: AppearanceValues;\n}\n\nfunction AppearanceForm({defaultValues}: AppearanceFormProps) {\n const form = useForm({defaultValues});\n const {watch, reset} = form;\n const saveChanges = useSaveAppearanceChanges();\n\n useEffect(() => {\n const subscription = watch(value => {\n appearanceState().preview.setValues(value as AppearanceValues);\n });\n return () => subscription.unsubscribe();\n }, [watch]);\n\n return (\n {\n saveChanges.mutate(values, {\n onSuccess: () => reset(values),\n });\n }}\n >\n
\n \n
\n \n \n \n
\n \n );\n}\n\ninterface HeaderProps {\n isLoading: boolean;\n}\nfunction Header({isLoading}: HeaderProps) {\n const {\n formState: {dirtyFields},\n } = useFormContext();\n const isDirty = Object.keys(dirtyFields).length;\n return (\n
\n \n \n \n
\n \n
\n \n {isDirty ? : }\n \n
\n );\n}\n","import {Link, useNavigate} from 'react-router-dom';\nimport {AppearanceValues} from '../../appearance-store';\nimport {Button} from '../../../../ui/buttons/button';\nimport {AddIcon} from '../../../../icons/material/Add';\nimport {Trans} from '../../../../i18n/trans';\nimport {useFieldArray} from 'react-hook-form';\nimport {AppearanceButton} from '../../appearance-button';\nimport {nanoid} from 'nanoid';\nimport {useTrans} from '../../../../i18n/use-trans';\nimport {message} from '../../../../i18n/message';\nimport {Fragment} from 'react';\n\nexport function MenuList() {\n const navigate = useNavigate();\n const {trans} = useTrans();\n const {fields, append} = useFieldArray<\n AppearanceValues,\n 'settings.menus',\n 'key'\n >({\n name: 'settings.menus',\n keyName: 'key',\n });\n\n return (\n \n
\n {fields.map((field, index) => (\n \n {field.name}\n \n ))}\n
\n
\n }\n size=\"xs\"\n onClick={() => {\n const id = nanoid(10);\n append({\n name: trans(\n message('New menu :number', {\n values: {number: fields.length + 1},\n })\n ),\n id,\n positions: [],\n items: [],\n });\n navigate(`${fields.length}`);\n }}\n >\n \n \n
\n
\n );\n}\n","import {useForm} from 'react-hook-form';\nimport {Accordion, AccordionItem} from '@common/ui/accordion/accordion';\nimport {Form} from '@common/ui/forms/form';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {MenuItemConfig} from '@common/core/settings/settings';\nimport {AddIcon} from '@common/icons/material/Add';\nimport {Button} from '@common/ui/buttons/button';\nimport {useAvailableRoutes} from '@common/admin/appearance/sections/menus/hooks/available-routes';\nimport {ucFirst} from '@common/utils/string/uc-first';\nimport {List, ListItem} from '@common/ui/list/list';\nimport {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';\nimport {Dialog} from '@common/ui/overlays/dialog/dialog';\nimport {DialogHeader} from '@common/ui/overlays/dialog/dialog-header';\nimport {DialogBody} from '@common/ui/overlays/dialog/dialog-body';\nimport {Trans} from '@common/i18n/trans';\nimport {useValueLists} from '@common/http/value-lists';\nimport {ReactNode} from 'react';\nimport {nanoid} from 'nanoid';\n\ninterface AddMenuItemDialogProps {\n title?: ReactNode;\n}\nexport function AddMenuItemDialog({\n title = ,\n}: AddMenuItemDialogProps) {\n const {data} = useValueLists(['menuItemCategories']);\n const categories = data?.menuItemCategories || [];\n const routeItems = useAvailableRoutes();\n\n return (\n \n {title}\n \n \n }\n bodyClassName=\"max-h-240 overflow-y-auto\"\n >\n \n \n }\n bodyClassName=\"max-h-240 overflow-y-auto\"\n >\n \n \n {categories.map(category => (\n }\n >\n \n \n ))}\n \n \n \n );\n}\n\nfunction AddCustomLink() {\n const form = useForm({\n defaultValues: {\n id: nanoid(6),\n type: 'link',\n target: '_blank',\n },\n });\n const {close} = useDialogContext();\n\n return (\n {\n close(value);\n }}\n >\n }\n className=\"mb-20\"\n />\n }\n className=\"mb-20\"\n />\n
\n \n
\n \n );\n}\n\ninterface AddRouteProps {\n items: Partial[];\n}\nfunction AddRoute({items}: AddRouteProps) {\n const {close} = useDialogContext();\n\n return (\n \n {items.map(item => {\n return (\n }\n onSelected={() => {\n if (item.label) {\n const last = item.label.split('/').pop();\n item.label = last ? ucFirst(last) : item.label;\n item.id = nanoid(6);\n }\n close(item);\n }}\n >\n {item.label}\n \n );\n })}\n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const DragIndicatorIcon = createSvgIcon(\n \n, 'DragIndicatorOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const DeleteIcon = createSvgIcon(\n \n, 'DeleteOutlined');\n","export default \"__VITE_ASSET__abcb02f6__\"","import {\n FieldArrayWithId,\n useFieldArray,\n UseFieldArrayReturn,\n useFormContext,\n} from 'react-hook-form';\nimport {Fragment, useEffect, useMemo, useRef} from 'react';\nimport {Link, useNavigate, useParams} from 'react-router-dom';\nimport {MenuSectionConfig} from '../../types/appearance-editor-config';\nimport {MenuItemConfig} from '@common/core/settings/settings';\nimport {\n appearanceState,\n AppearanceValues,\n useAppearanceStore,\n} from '../../appearance-store';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {Button} from '@common/ui/buttons/button';\nimport {AddMenuItemDialog} from '@common/admin/appearance/sections/menus/add-menu-item-dialog';\nimport {AppearanceButton} from '@common/admin/appearance/appearance-button';\nimport {AddIcon} from '@common/icons/material/Add';\nimport {DragIndicatorIcon} from '@common/icons/material/DragIndicator';\nimport {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';\nimport {IllustratedMessage} from '@common/ui/images/illustrated-message';\nimport {SvgImage} from '@common/ui/images/svg-image/svg-image';\nimport {DeleteIcon} from '@common/icons/material/Delete';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {Option} from '../../../../ui/forms/select/select';\nimport {Trans} from '@common/i18n/trans';\nimport dropdownMenu from './dropdown-menu.svg';\nimport {FormChipField} from '@common/ui/forms/input-field/chip-field/form-chip-field';\nimport {\n useSortable,\n UseSortableProps,\n} from '@common/ui/interactions/dnd/sortable/use-sortable';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {createSvgIconFromTree} from '@common/icons/create-svg-icon';\nimport {useSettings} from '@common/core/settings/use-settings';\n\nexport function MenuEditor() {\n const {menuIndex} = useParams();\n const navigate = useNavigate();\n\n const {getValues} = useFormContext();\n const formPath = `settings.menus.${menuIndex!}` as 'settings.menus.0';\n const menu = getValues(formPath);\n\n useEffect(() => {\n // go to menu list, if menu can't be found\n if (!menu) {\n navigate('/admin/appearance/menus');\n } else {\n appearanceState().preview.setHighlight(`[data-menu-id=\"${menu.id}\"]`);\n }\n }, [navigate, menu]);\n\n if (!menu) {\n return null;\n }\n\n return ;\n}\n\ninterface MenuEditorFormProps {\n formPath: 'settings.menus.0';\n}\nfunction MenuEditorSection({formPath}: MenuEditorFormProps) {\n const {\n site: {has_mobile_app},\n } = useSettings();\n const menuSectionConfig = useAppearanceStore(\n s => s.config?.sections.menus.config,\n ) as MenuSectionConfig;\n\n const menuPositions = useMemo(() => {\n const positions = [...menuSectionConfig?.positions];\n if (has_mobile_app) {\n positions.push('mobile-app-about');\n }\n return positions.map(position => ({\n key: position,\n name: position.replaceAll('-', ' '),\n }));\n }, [menuSectionConfig, has_mobile_app]);\n\n const fieldArray = useFieldArray<\n AppearanceValues,\n `settings.menus.0.items`,\n 'key'\n >({\n name: `${formPath}.items`,\n keyName: 'key',\n });\n\n return (\n \n
\n }\n className=\"mb-20\"\n autoFocus\n />\n }\n description={\n \n }\n >\n {menuPositions.map(item => (\n \n ))}\n \n
\n \n
\n \n
\n
\n );\n}\n\ninterface ItemListProps {\n fieldArray: UseFieldArrayReturn<\n AppearanceValues,\n 'settings.menus.0.items',\n 'key'\n >;\n}\nfunction MenuItemsManager({fieldArray: {append, fields, move}}: ItemListProps) {\n const navigate = useNavigate();\n\n return (\n \n
\n \n {\n if (menuItemConfig) {\n append({...menuItemConfig});\n navigate(`items/${fields.length}`);\n }\n }}\n >\n }\n >\n \n \n \n \n
\n
\n {fields.map((item, index) => (\n {\n move(oldIndex, newIndex);\n }}\n />\n ))}\n {!fields.length ? (\n }\n title={}\n description={\n \n }\n />\n ) : null}\n
\n
\n );\n}\n\nfunction DeleteMenuTrigger() {\n const navigate = useNavigate();\n const {menuIndex} = useParams();\n const {fields, remove} = useFieldArray<\n AppearanceValues,\n 'settings.menus',\n 'key'\n >({\n name: 'settings.menus',\n keyName: 'key',\n });\n if (!menuIndex) return null;\n const menu = fields[+menuIndex];\n\n return (\n {\n if (isConfirmed) {\n const index = fields.findIndex(m => m.id === menu.id);\n remove(index);\n navigate('/admin/appearance/menus');\n }\n }}\n >\n }\n >\n \n \n }\n body={\n \n }\n confirm={}\n />\n \n );\n}\n\ninterface MenuListItemProps {\n item: MenuItemConfig;\n items: FieldArrayWithId[];\n index: number;\n onSortEnd: UseSortableProps['onSortEnd'];\n}\nfunction MenuListItem({item, items, index, onSortEnd}: MenuListItemProps) {\n const ref = useRef(null);\n const {sortableProps, dragHandleRef} = useSortable({\n item,\n items,\n type: 'menuEditorSortable',\n ref,\n onSortEnd,\n strategy: 'liveSort',\n });\n\n const Icon = item.icon && createSvgIconFromTree(item.icon);\n const iconOnlyLabel = (\n
\n {Icon && }\n ()\n
\n );\n\n return (\n \n \n
\n \n \n \n
{item.label || iconOnlyLabel}
\n
\n \n
\n );\n}\n","import {useFieldArray, useFormContext} from 'react-hook-form';\nimport {Fragment, useEffect} from 'react';\nimport {appearanceState, AppearanceValues} from '../../appearance-store';\nimport {Button} from '@common/ui/buttons/button';\nimport {DeleteIcon} from '../../../../icons/material/Delete';\nimport {ConfirmationDialog} from '../../../../ui/overlays/dialog/confirmation-dialog';\nimport {DialogTrigger} from '../../../../ui/overlays/dialog/dialog-trigger';\nimport {Trans} from '../../../../i18n/trans';\nimport {useNavigate} from '../../../../utils/hooks/use-navigate';\nimport {MenuItemForm} from '../../../menus/menu-item-form';\nimport {useParams} from 'react-router-dom';\nimport {MenuItemConfig} from '../../../../core/settings/settings';\n\nexport function MenuItemEditor() {\n const {menuIndex, menuItemIndex} = useParams();\n const navigate = useNavigate();\n\n const {getValues} = useFormContext();\n\n const formPath = `settings.menus.${menuIndex}.items.${menuItemIndex}`;\n const item = getValues(formPath as any);\n\n // go to menu editor, if menu item can't be found\n useEffect(() => {\n if (!item) {\n //navigate(`../`);\n } else {\n appearanceState().preview.setHighlight(\n `[data-menu-item-id=\"${item.id}\"]`\n );\n }\n }, [navigate, item]);\n\n // only render form when menu and item are available to avoid issues with hook form default values\n if (!item || menuItemIndex == null) {\n return null;\n }\n\n return ;\n}\n\ninterface MenuItemEditorSectionProps {\n formPath: string;\n}\nfunction MenuItemEditorSection({formPath}: MenuItemEditorSectionProps) {\n return (\n \n \n
\n \n
\n
\n );\n}\n\nfunction DeleteItemTrigger() {\n const navigate = useNavigate();\n const {menuIndex, menuItemIndex} = useParams();\n const {fields, remove} = useFieldArray({\n name: `settings.menus.${+menuIndex!}.items`,\n });\n\n if (!menuItemIndex) return null;\n\n const item = fields[+menuItemIndex] as MenuItemConfig;\n\n return (\n {\n if (isConfirmed) {\n if (menuItemIndex) {\n remove(+menuItemIndex);\n navigate(`/admin/appearance/menus/${menuIndex}`);\n }\n }\n }}\n >\n }\n >\n \n \n }\n body={\n \n }\n confirm={}\n />\n \n );\n}\n","import {appearanceState, useAppearanceStore} from '../appearance-store';\nimport {FormImageSelector} from '@common/ui/images/image-selector';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {Trans} from '@common/i18n/trans';\nimport {Fragment, ReactNode} from 'react';\nimport {Settings} from '../../../core/settings/settings';\n\nexport function GeneralSection() {\n return (\n \n }\n description={\n \n }\n type=\"favicon\"\n />\n }\n description={}\n type=\"logo_light\"\n />\n }\n description={\n \n }\n type=\"logo_dark\"\n />\n }\n description={\n \n }\n type=\"logo_light_mobile\"\n />\n }\n description={\n \n }\n type=\"logo_dark_mobile\"\n />\n \n \n \n );\n}\n\ninterface ImageSelectorProps {\n label: ReactNode;\n description: ReactNode;\n type: keyof Settings['branding'];\n}\nfunction BrandingImageSelector({label, description, type}: ImageSelectorProps) {\n const defaultValue = useAppearanceStore(\n s => s.defaults?.settings.branding[type]\n );\n return (\n {\n appearanceState().preview.setHighlight('[data-logo=\"navbar\"]');\n }}\n />\n );\n}\nfunction SiteNameTextField() {\n return (\n }\n />\n );\n}\n\nfunction SiteDescriptionTextArea() {\n return (\n }\n />\n );\n}\n","export function randomNumber(min: number = 1, max: number = 10000) {\n const randomBuffer = new Uint32Array(1);\n\n window.crypto.getRandomValues(randomBuffer);\n\n const number = randomBuffer[0] / (0xffffffff + 1);\n\n min = Math.ceil(min);\n max = Math.floor(max);\n return Math.floor(number * (max - min + 1)) + min;\n}\n","import {NavLink, useNavigate} from 'react-router-dom';\nimport {Fragment, useEffect} from 'react';\nimport {appearanceState, AppearanceValues} from '../../appearance-store';\nimport {AppearanceButton} from '../../appearance-button';\nimport {Button} from '../../../../ui/buttons/button';\nimport {AddIcon} from '../../../../icons/material/Add';\nimport {randomNumber} from '../../../../utils/string/random-number';\nimport {Trans} from '../../../../i18n/trans';\nimport {useFieldArray} from 'react-hook-form';\nimport {useTrans} from '../../../../i18n/use-trans';\nimport {message} from '../../../../i18n/message';\nimport {useBootstrapData} from '../../../../core/bootstrap-data/bootstrap-data-context';\n\nexport function ThemeList() {\n const {trans} = useTrans();\n const navigate = useNavigate();\n const {\n data: {themes},\n } = useBootstrapData();\n const {fields, append} = useFieldArray<\n AppearanceValues,\n 'appearance.themes.all',\n 'key'\n >({\n name: 'appearance.themes.all',\n keyName: 'key',\n });\n\n useEffect(() => {\n if (themes.selectedThemeId) {\n appearanceState().preview.setActiveTheme(themes.selectedThemeId);\n }\n }, [themes.selectedThemeId]);\n\n return (\n \n
\n }\n onClick={() => {\n const lightThemeColors =\n appearanceState().defaults?.appearance.themes.light!;\n append({\n id: randomNumber(),\n name: trans(message('New theme')),\n values: lightThemeColors,\n });\n navigate(`${fields.length + 1}`);\n }}\n >\n \n \n
\n {fields.map((field, index) => (\n \n {field.name}\n \n ))}\n
\n );\n}\n","import React, {MutableRefObject, ReactNode, Suspense, useState} from 'react';\nimport {Dialog} from '../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../ui/overlays/dialog/dialog-header';\nimport {Trans} from '../i18n/trans';\nimport {DialogBody} from '../ui/overlays/dialog/dialog-body';\nimport {ProgressCircle} from '../ui/progress/progress-circle';\nimport {useDialogContext} from '../ui/overlays/dialog/dialog-context';\nimport {DialogFooter} from '../ui/overlays/dialog/dialog-footer';\nimport {Button} from '../ui/buttons/button';\nimport type ReactAce from 'react-ace';\n\nconst AceEditor = React.lazy(() => import('./ace-editor'));\n\ninterface TextEditorSourcecodeDialogProps {\n defaultValue: string;\n mode?: 'css' | 'html' | 'php_laravel_blade';\n title: ReactNode;\n onSave?: (value?: string) => void;\n isSaving?: boolean;\n footerStartAction?: ReactNode;\n beautify?: boolean;\n editorRef?: MutableRefObject;\n}\nexport function AceDialog({\n defaultValue,\n mode = 'html',\n title,\n onSave,\n isSaving,\n footerStartAction,\n beautify,\n editorRef,\n}: TextEditorSourcecodeDialogProps) {\n const [value, setValue] = useState(defaultValue);\n const [isValid, setIsValid] = useState(true);\n\n return (\n \n {title}\n \n \n \n \n }\n >\n setValue(newValue)}\n defaultValue={value || ''}\n onIsValidChange={setIsValid}\n editorRef={editorRef}\n />\n \n \n \n \n );\n}\n\ninterface FooterProps {\n disabled: boolean | undefined;\n value?: string;\n onSave?: (value?: string) => void;\n startAction?: ReactNode;\n}\nfunction Footer({disabled, value, onSave, startAction}: FooterProps) {\n const {close} = useDialogContext();\n return (\n \n \n {\n if (onSave) {\n onSave(value);\n } else {\n close(value);\n }\n }}\n >\n \n \n \n );\n}\n","import {useQuery} from '@tanstack/react-query';\nimport {apiClient} from '@common/http/query-client';\n\nexport function useSeoTags(name: string | string[]) {\n return useQuery({\n queryKey: ['admin', 'seo-tags', name],\n queryFn: () => fetchTags(name),\n });\n}\n\nfunction fetchTags(name: string | string[]) {\n return apiClient\n .get<\n Record<\n string,\n {\n custom: string | null;\n original: string;\n }\n >\n >(`admin/appearance/seo-tags/${name}`)\n .then(response => response.data);\n}\n","import {useMutation, useQueryClient} from '@tanstack/react-query';\nimport {apiClient} from '@common/http/query-client';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\n\ninterface Response extends BackendResponse {}\n\nexport function useUpdateSeoTags(name: string) {\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: (payload: {tags: string}) => updateTags(name, payload.tags),\n onSuccess: async () => {\n await queryClient.invalidateQueries({\n queryKey: ['admin', 'seo-tags', name],\n });\n toast(message('Updated SEO tags'));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction updateTags(name: string, tags: string): Promise {\n return apiClient\n .put(`admin/appearance/seo-tags/${name}`, {tags})\n .then(r => r.data);\n}\n","import {Fragment, useRef} from 'react';\nimport {Trans} from '@common/i18n/trans';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {AppearanceButton} from '@common/admin/appearance/appearance-button';\nimport {AceDialog} from '@common/ace-editor/ace-dialog';\nimport mergedAppearanceConfig from '@common/admin/appearance/config/merged-appearance-config';\nimport {SeoSettingsSectionConfig} from '@common/admin/appearance/types/appearance-editor-config';\nimport {MessageDescriptor} from '@common/i18n/message-descriptor';\nimport {useSeoTags} from '@common/admin/appearance/sections/seo/use-seo-tags';\nimport {useUpdateSeoTags} from '@common/admin/appearance/sections/seo/use-update-seo-tags';\nimport {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';\nimport {FullPageLoader} from '@common/ui/progress/full-page-loader';\nimport {Button} from '@common/ui/buttons/button';\nimport type ReactAce from 'react-ace';\n\nconst pages =\n (\n mergedAppearanceConfig.sections['seo-settings']\n .config as SeoSettingsSectionConfig\n )?.pages || [];\n\nconst names = pages.map(page => page.key);\n\nexport function SeoSection() {\n const {isLoading} = useSeoTags(names);\n\n if (isLoading) {\n return ;\n }\n\n return (\n \n {pages.map(page => (\n \n ))}\n \n );\n}\n\ninterface TagEditorTriggerProps {\n label: MessageDescriptor;\n name: string;\n}\nfunction TagEditorTrigger({label, name}: TagEditorTriggerProps) {\n const {data, isLoading} = useSeoTags(names);\n\n return (\n \n \n \n \n {data ? : null}\n \n );\n}\n\ninterface TagsEditorDialogProps {\n name: string;\n value: {custom: string | null; original: string};\n}\nfunction TagsEditorDialog({name, value}: TagsEditorDialogProps) {\n const {close} = useDialogContext();\n const updateTags = useUpdateSeoTags(name);\n const editorRef = useRef(null);\n\n const resetButton = (\n {\n if (editorRef.current) {\n editorRef.current.editor.setValue(value.original);\n }\n }}\n >\n \n \n );\n\n return (\n }\n footerStartAction={resetButton}\n editorRef={editorRef}\n defaultValue={value.custom || value.original}\n isSaving={updateTags.isPending}\n beautify={false}\n onSave={newValue => {\n if (newValue != null) {\n updateTags.mutate(\n {tags: newValue},\n {\n onSuccess: () => close(),\n },\n );\n }\n }}\n />\n );\n}\n","import {AppearanceButton} from '@common/admin/appearance/appearance-button';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {Trans} from '@common/i18n/trans';\nimport {useFormContext} from 'react-hook-form';\nimport {\n appearanceState,\n AppearanceValues,\n} from '@common/admin/appearance/appearance-store';\nimport {AceDialog} from '@common/ace-editor/ace-dialog';\nimport {Fragment} from 'react';\n\nexport function CustomCodeSection() {\n return (\n \n \n \n \n );\n}\n\ninterface CustomCodeDialogTriggerProps {\n mode: 'html' | 'css';\n}\nfunction CustomCodeDialogTrigger({mode}: CustomCodeDialogTriggerProps) {\n const {getValues} = useFormContext();\n const {setValue} = useFormContext();\n\n const title =\n mode === 'html' ? (\n \n ) : (\n \n );\n\n return (\n {\n if (newValue != null) {\n setValue(`appearance.custom_code.${mode}`, newValue, {\n shouldDirty: true,\n });\n appearanceState().preview.setCustomCode(mode, newValue);\n }\n }}\n >\n {title}\n \n \n );\n}\n","export default \"__VITE_ASSET__8acde003__\"","import {Permission} from './permission';\nimport {Subscription} from '../billing/subscription';\nimport {Role} from './role';\nimport {SocialProfile} from './social-profile';\nimport {AccessToken} from './access-token';\nimport type {ActiveSession} from '@common/auth/ui/account-settings/sessions-panel/requests/use-user-sessions';\n\nexport const USER_MODEL = 'user';\n\nexport interface User {\n id: number;\n display_name: string;\n username?: string;\n first_name?: string;\n last_name?: string;\n avatar?: string;\n email_verified_at: string;\n permissions?: Permission[];\n email: string;\n password: string;\n language: string;\n timezone: string;\n country: string;\n created_at: string;\n updated_at: string;\n subscriptions?: Omit[];\n roles: Role[];\n social_profiles: SocialProfile[];\n tokens?: AccessToken[];\n has_password: boolean;\n available_space: number | null;\n unread_notifications_count?: number;\n card_last_four?: number;\n card_brand?: string;\n card_expires?: string;\n model_type: typeof USER_MODEL;\n banned_at?: string;\n followed_users?: Omit[];\n followers_count?: number;\n followed_users_count?: number;\n followers?: Omit[];\n last_login?: ActiveSession;\n bans?: {\n id: number;\n comment: string;\n expired_at?: string;\n }[];\n two_factor_confirmed_at?: string;\n two_factor_recovery_codes?: string[];\n}\n","import {\n BackendFilter,\n FilterControlType,\n FilterOperator,\n} from '../../datatable/filters/backend-filter';\nimport {message} from '../../i18n/message';\nimport {USER_MODEL} from '../../auth/user';\nimport {SiteConfigContextValue} from '@common/core/settings/site-config-context';\nimport {\n createdAtFilter,\n updatedAtFilter,\n} from '@common/datatable/filters/timestamp-filters';\n\nexport const CustomPageDatatableFilters = (\n config: SiteConfigContextValue\n): BackendFilter[] => {\n const dynamicFilters: BackendFilter[] =\n config.customPages.types.length > 1\n ? [\n {\n control: {\n type: FilterControlType.Select,\n defaultValue: 'default',\n options: config.customPages.types.map(type => ({\n value: type.type,\n label: type.label,\n key: type.type,\n })),\n },\n\n key: 'type',\n label: message('Type'),\n description: message('Type of the page'),\n defaultOperator: FilterOperator.eq,\n },\n ]\n : [];\n\n return [\n {\n key: 'user_id',\n label: message('User'),\n description: message('User page was created by'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.SelectModel,\n model: USER_MODEL,\n },\n },\n ...dynamicFilters,\n createdAtFilter({\n description: message('Date page was created'),\n }),\n updatedAtFilter({\n description: message('Date page was last updated'),\n }),\n ];\n};\n","import {ColumnConfig} from '@common/datatable/column-config';\nimport {CustomPage} from '@common/admin/custom-pages/custom-page';\nimport {Trans} from '@common/i18n/trans';\nimport {Link} from 'react-router-dom';\nimport {LinkStyle} from '@common/ui/buttons/external-link';\nimport {NameWithAvatar} from '@common/datatable/column-templates/name-with-avatar';\nimport {FormattedDate} from '@common/i18n/formatted-date';\nimport React from 'react';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {EditIcon} from '@common/icons/material/Edit';\n\nexport const CustomPageDatatableColumns: ColumnConfig[] = [\n {\n key: 'slug',\n allowsSorting: true,\n width: 'flex-2 min-w-200',\n visibleInMode: 'all',\n header: () => ,\n body: page => (\n \n {page.slug}\n \n ),\n },\n {\n key: 'user_id',\n allowsSorting: true,\n width: 'flex-2 min-w-140',\n header: () => ,\n body: page =>\n page.user && (\n \n ),\n },\n {\n key: 'type',\n maxWidth: 'max-w-100',\n header: () => ,\n body: page => ,\n },\n {\n key: 'updated_at',\n allowsSorting: true,\n width: 'w-100',\n header: () => ,\n body: page => ,\n },\n {\n key: 'actions',\n header: () => ,\n hideHeader: true,\n align: 'end',\n width: 'w-84 flex-shrink-0',\n visibleInMode: 'all',\n body: page => (\n \n \n \n ),\n },\n];\n","import React, {useContext, useMemo} from 'react';\nimport {Link} from 'react-router-dom';\nimport {DataTablePage} from '../../datatable/page/data-table-page';\nimport {Trans} from '../../i18n/trans';\nimport {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';\nimport articlesSvg from './articles.svg';\nimport {DataTableAddItemButton} from '../../datatable/data-table-add-item-button';\nimport {CustomPageDatatableFilters} from './custom-page-datatable-filters';\nimport {DeleteSelectedItemsAction} from '../../datatable/page/delete-selected-items-action';\nimport {CustomPageDatatableColumns} from '@common/admin/custom-pages/custom-page-datatable-columns';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\n\nexport function CustomPageDatablePage() {\n const config = useContext(SiteConfigContext);\n const filters = useMemo(() => {\n return CustomPageDatatableFilters(config);\n }, [config]);\n\n return (\n }\n filters={filters}\n columns={CustomPageDatatableColumns}\n queryParams={{with: 'user'}}\n actions={}\n selectedActions={}\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n );\n}\n\nfunction Actions() {\n return (\n \n \n \n );\n}\n","import {SettingsNavItem} from '@common/admin/settings/settings-nav-config';\nimport {message} from '@common/i18n/message';\n\nexport const AppSettingsNavConfig: SettingsNavItem[] = [\n {label: message('Local search'), to: 'search'},\n {label: message('Content'), to: 'content'},\n {label: message('Videos'), to: 'videos'},\n];\n","import {AppSettingsNavConfig} from '@app/admin/settings/app-settings-nav-config';\nimport {message} from '../../i18n/message';\nimport {MessageDescriptor} from '../../i18n/message-descriptor';\nimport {To} from 'react-router-dom';\nimport {getBootstrapData} from '@common/core/bootstrap-data/use-backend-bootstrap-data';\n\nexport interface SettingsNavItem {\n label: MessageDescriptor;\n to: To;\n}\n\nconst filteredSettingsNavConfig: (SettingsNavItem | false)[] = [\n {label: message('General'), to: 'general'},\n ...AppSettingsNavConfig,\n getBootstrapData().settings.billing.integrated && {\n label: message('Subscriptions'),\n to: 'subscriptions',\n },\n {label: message('Localization'), to: 'localization'},\n {\n label: message('Authentication'),\n to: 'authentication',\n },\n {label: message('Uploading'), to: 'uploading'},\n {label: message('Outgoing email'), to: 'outgoing-email'},\n {label: message('Cache'), to: 'cache'},\n {label: message('Analytics'), to: 'analytics'},\n {label: message('Logging'), to: 'logging'},\n {label: message('Queue'), to: 'queue'},\n {label: message('Recaptcha'), to: 'recaptcha'},\n {label: message('GDPR'), to: 'gdpr'},\n {\n label: message('Menus'),\n to: '/admin/appearance/menus',\n },\n {\n label: message('Seo'),\n to: '/admin/appearance/seo-settings',\n },\n {\n label: message('Themes'),\n to: '/admin/appearance/themes',\n },\n].filter(Boolean);\n\nexport const SettingsNavConfig = filteredSettingsNavConfig as SettingsNavItem[];\n","import clsx from 'clsx';\nimport {NavLink, Outlet, useLocation, useNavigate} from 'react-router-dom';\nimport {SettingsNavConfig} from './settings-nav-config';\nimport {useIsMobileMediaQuery} from '../../utils/hooks/is-mobile-media-query';\nimport {Option, Select} from '../../ui/forms/select/select';\nimport {Trans} from '../../i18n/trans';\nimport {StaticPageTitle} from '../../seo/static-page-title';\n\ninterface Props {\n className?: string;\n}\nexport function SettingsLayout({className}: Props) {\n const isMobile = useIsMobileMediaQuery();\n return (\n \n \n \n \n {isMobile ? : }\n
\n \n
\n \n );\n}\n\nfunction MobileNav() {\n const {pathname} = useLocation();\n const navigate = useNavigate();\n const value = pathname.split('/').pop();\n\n return (\n {\n navigate(newPage as string);\n }}\n >\n {SettingsNavConfig.map(item => (\n \n ))}\n \n );\n}\n\nfunction DesktopNav() {\n return (\n
\n {SettingsNavConfig.map(item => (\n \n clsx(\n 'mb-8 block whitespace-nowrap rounded-button p-14 text-sm transition-bg-color',\n isActive\n ? 'bg-primary/6 font-semibold text-primary'\n : 'hover:bg-hover',\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 {AdminSettings} from '../admin-settings';\nimport {apiClient} from '@common/http/query-client';\n\nexport interface FetchAdminSettingsResponse\n extends BackendResponse,\n AdminSettings {}\n\nexport function useAdminSettings() {\n return useQuery({\n queryKey: ['fetchAdminSettings'],\n queryFn: () => fetchAdminSettings(),\n // prevent automatic re-fetching so diffing with previous settings work properly\n staleTime: Infinity,\n });\n}\n\nfunction fetchAdminSettings(): Promise {\n return apiClient.get('settings').then(response => response.data);\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\nfunction GenerateSitemap(): Promise {\n return apiClient.post('sitemap/generate').then(r => r.data);\n}\n\nexport function useGenerateSitemap() {\n return useMutation({\n mutationFn: () => GenerateSitemap(),\n onSuccess: () => {\n toast(message('Sitemap generated'));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {diff} from 'deep-object-diff';\nimport dot from 'dot-object';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {toast} from '@common/ui/toast/toast';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {AdminSettings} from '@common/admin/settings/admin-settings';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {useAdminSettings} from '@common/admin/settings/requests/use-admin-settings';\nimport {message} from '@common/i18n/message';\n\ninterface Response extends BackendResponse {}\n\nexport interface AdminSettingsWithFiles {\n files?: Record;\n client?: Partial;\n server?: Partial;\n}\n\nexport function useUpdateAdminSettings(\n form: UseFormReturn,\n) {\n const {data: original} = useAdminSettings();\n\n return useMutation({\n mutationFn: (props: AdminSettingsWithFiles) => {\n //need to convert these to json, otherwise only single key from object would be sent due to diffing\n if (props.client?.cookie_notice?.button) {\n props.client.cookie_notice.button = JSON.stringify(\n props.client.cookie_notice.button,\n ) as any;\n }\n if (props.client?.registration?.policies) {\n props.client.registration.policies = JSON.stringify(\n props.client.registration.policies,\n ) as any;\n }\n if ((props.client as any)?.artistPage?.tabs) {\n (props.client as any).artistPage.tabs = JSON.stringify(\n (props.client as any).artistPage.tabs,\n ) as any;\n }\n if ((props.client as any)?.title_page?.sections) {\n (props.client as any).title_page.sections = JSON.stringify(\n (props.client as any).title_page.sections,\n ) as any;\n }\n if ((props.client as any)?.incoming_email) {\n (props.client as any).incoming_email = JSON.stringify(\n (props.client as any).incoming_email,\n ) as any;\n }\n if ((props.client as any)?.publish?.default_credentials) {\n (props.client as any).publish.default_credentials = JSON.stringify(\n (props.client as any).publish.default_credentials,\n ) as any;\n }\n\n const client = props.client ? diff(original!.client, props.client) : null;\n const server = props.server ? diff(original!.server, props.server) : null;\n return updateAdminSettings({\n client,\n server,\n files: props.files,\n } as AdminSettings);\n },\n onSuccess: () => {\n toast(message('Settings updated'), {\n position: 'bottom-right',\n });\n queryClient.invalidateQueries({queryKey: ['fetchAdminSettings']});\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction updateAdminSettings({\n client,\n server,\n files,\n}: AdminSettingsWithFiles): Promise {\n const formData = new FormData();\n if (client) {\n formData.set('client', JSON.stringify(dot.dot(client)));\n }\n if (server) {\n formData.set('server', JSON.stringify(dot.dot(server)));\n }\n Object.entries(files || {}).forEach(([key, file]) => {\n formData.set(key, file);\n });\n return apiClient\n .post('settings', formData, {\n headers: {\n 'Content-Type': 'multipart/form-data',\n },\n })\n .then(r => r.data);\n}\n","import {FieldErrors, useForm} from 'react-hook-form';\nimport {Fragment, ReactNode} from 'react';\nimport {\n AdminSettingsWithFiles,\n useUpdateAdminSettings,\n} from './requests/update-admin-settings';\nimport {AdminSettings} from './admin-settings';\nimport {useAdminSettings} from './requests/use-admin-settings';\nimport {Form} from '../../ui/forms/form';\nimport {Button} from '../../ui/buttons/button';\nimport {ProgressCircle} from '../../ui/progress/progress-circle';\nimport {ProgressBar} from '../../ui/progress/progress-bar';\nimport {Trans} from '../../i18n/trans';\n\ninterface Props {\n title: ReactNode;\n description: ReactNode;\n children: ReactNode;\n transformValues?: (values: AdminSettingsWithFiles) => AdminSettingsWithFiles;\n}\nexport function SettingsPanel({\n title,\n description,\n children,\n transformValues,\n}: Props) {\n const {data} = useAdminSettings();\n\n return (\n
\n
\n

{title}

\n
{description}
\n
\n {data ? (\n \n {children}\n \n ) : (\n \n )}\n
\n );\n}\n\ninterface FormWrapperProps {\n children: ReactNode;\n defaultValues: AdminSettings;\n transformValues?: (values: AdminSettingsWithFiles) => AdminSettingsWithFiles;\n}\nfunction FormWrapper({\n children,\n defaultValues,\n transformValues,\n}: FormWrapperProps) {\n const form = useForm({defaultValues});\n const updateSettings = useUpdateAdminSettings(form);\n return (\n \n {\n // clear group errors, because hook form won't automatically\n // clear errors that are not bound to a specific form field\n const errors = form.formState.errors as FieldErrors;\n const keys = Object.keys(errors).filter(key => {\n return key.endsWith('_group');\n });\n form.clearErrors(keys as any);\n }}\n onSubmit={value => {\n value = transformValues ? transformValues(value) : value;\n updateSettings.mutate(value);\n }}\n >\n {children}\n
\n \n \n \n
\n \n {updateSettings.isPending && (\n \n )}\n \n );\n}\n","export function SettingsSeparator() {\n return
;\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const LinkIcon = createSvgIcon(\n \n, 'LinkOutlined');\n","import clsx from 'clsx';\nimport {LinkIcon} from '../../icons/material/Link';\nimport {ExternalLink} from '../../ui/buttons/external-link';\nimport {Trans} from '../../i18n/trans';\nimport {useSettings} from '../../core/settings/use-settings';\n\ninterface LearnMoreLinkProps {\n link: string;\n className?: string;\n}\nexport function LearnMoreLink({link, className}: LearnMoreLinkProps) {\n const {site} = useSettings();\n if (site.hide_docs_button) {\n return null;\n }\n return (\n
\n \n \n \n \n
\n );\n}\n","import {useAdminSettings} from '../requests/use-admin-settings';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {FormSelect, Option} from '../../../ui/forms/select/select';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {Button} from '@common/ui/buttons/button';\nimport {useGenerateSitemap} from '../generate-sitemap';\nimport {ExternalLink} from '@common/ui/buttons/external-link';\nimport {SettingsPanel} from '../settings-panel';\nimport {SettingsSeparator} from '../settings-separator';\nimport {LearnMoreLink} from '../learn-more-link';\nimport {Trans} from '@common/i18n/trans';\nimport {Fragment, useContext} from 'react';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {useBootstrapData} from '@common/core/bootstrap-data/bootstrap-data-context';\nimport {useValueLists} from '@common/http/value-lists';\nimport {useFormContext} from 'react-hook-form';\nimport {AdminSettingsWithFiles} from '@common/admin/settings/requests/update-admin-settings';\n\nexport function GeneralSettings() {\n return (\n }\n description={\n \n }\n >\n \n \n \n \n \n \n \n \n );\n}\n\nfunction SiteUrlSection() {\n const {data} = useAdminSettings();\n\n if (!data) return null;\n\n let append = null;\n const server = data!.server;\n const isInvalid = server.newAppUrl && server.newAppUrl !== server.app_url;\n if (isInvalid) {\n append = (\n
\n {chunks},\n }}\n message=\"Base site url is set as :baseUrl in configuration, but current url is :currentUrl. It is recommended to set the primary url you want to use in configuration file and then redirect all other url versions to this primary version via cpanel or .htaccess file.\"\n />\n
\n );\n }\n\n return (\n \n }\n description={\n \n }\n />\n {append}\n \n );\n}\n\nfunction HomepageSection() {\n const {watch} = useFormContext();\n const {homepage} = useContext(SiteConfigContext);\n const {data} = useValueLists(['menuItemCategories']);\n const selectedType = watch('client.homepage.type');\n\n return (\n
\n }\n description={\n \n }\n >\n {homepage.options.map(option => (\n \n ))}\n {data?.menuItemCategories?.map(category => (\n \n ))}\n \n {data?.menuItemCategories?.map(category => {\n return selectedType === category.type ? (\n \n }\n >\n {category.items.map(item => (\n \n ))}\n \n ) : null;\n })}\n
\n );\n}\n\nfunction ThemeSection() {\n const {\n data: {themes},\n } = useBootstrapData();\n return (\n \n }\n description={\n \n }\n >\n \n {themes.all.map(theme => (\n \n ))}\n \n \n }\n >\n \n \n \n );\n}\n\nfunction SitemapSection() {\n const generateSitemap = useGenerateSitemap();\n const {base_url} = useSettings();\n\n const url = `${base_url}/storage/sitemaps/sitemap-index.xml`;\n const link = {url};\n\n return (\n <>\n {\n generateSitemap.mutate();\n }}\n >\n \n \n
\n \n
\n \n );\n}\n","import {parseColor} from '@react-stately/color';\n\nexport function colorToThemeValue(color: string): string {\n return parseColor(color)\n .toString('rgb')\n .replace('rgb(', '')\n .replace(')', '')\n .replace(/, ?/g, ' ');\n}\n","import {useForm, useFormContext} from 'react-hook-form';\nimport {useEffect} from 'react';\nimport {TuneIcon} from '../../../../icons/material/Tune';\nimport {Button} from '../../../../ui/buttons/button';\nimport {CssTheme} from '../../../../ui/themes/css-theme';\nimport {FormTextField} from '../../../../ui/forms/input-field/text-field/text-field';\nimport {FormSwitch} from '../../../../ui/forms/toggle/switch';\nimport {AppearanceValues} from '../../appearance-store';\nimport {DialogTrigger} from '../../../../ui/overlays/dialog/dialog-trigger';\nimport {DialogFooter} from '../../../../ui/overlays/dialog/dialog-footer';\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 {Trans} from '../../../../i18n/trans';\nimport {Form} from '../../../../ui/forms/form';\nimport {useParams} from 'react-router-dom';\n\nexport function ThemeSettingsDialogTrigger() {\n const {getValues, setValue} = useFormContext();\n const {themeIndex} = useParams();\n const theme = getValues(`appearance.themes.all.${+themeIndex!}`);\n\n return (\n {\n if (!value) return;\n\n getValues('appearance.themes.all').forEach((currentTheme, index) => {\n // update changed theme\n if (currentTheme.id === value.id) {\n setValue(`appearance.themes.all.${index}`, value, {\n shouldDirty: true,\n });\n return;\n }\n\n // unset \"default_light\" and \"default_dark\" on other themes\n if (value.default_light) {\n setValue(\n `appearance.themes.all.${index}`,\n {...currentTheme, default_light: false},\n {shouldDirty: true}\n );\n return;\n }\n if (value.default_dark) {\n setValue(\n `appearance.themes.all.${index}`,\n {...currentTheme, default_dark: false},\n {shouldDirty: true}\n );\n return;\n }\n });\n }}\n >\n }\n >\n \n \n \n \n );\n}\n\ninterface SettingsDialogProps {\n theme: CssTheme;\n}\nfunction SettingsDialog({theme}: SettingsDialogProps) {\n const form = useForm({defaultValues: theme});\n const {close, formId} = useDialogContext();\n\n useEffect(() => {\n const subscription = form.watch((value, {name}) => {\n // theme can only be set as either light or dark default\n if (name === 'default_light' && value.default_light) {\n form.setValue('default_dark', false);\n }\n if (name === 'default_dark' && value.default_dark) {\n form.setValue('default_light', false);\n }\n });\n return () => subscription.unsubscribe();\n }, [form]);\n\n return (\n \n \n \n \n \n {\n close(values);\n }}\n >\n }\n className=\"mb-30\"\n autoFocus\n />\n \n }\n >\n \n \n \n }\n >\n \n \n \n }\n >\n \n \n \n \n \n {\n close();\n }}\n >\n \n \n \n \n \n \n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const RestartAltIcon = createSvgIcon(\n \n, 'RestartAltOutlined');\n","import {Fragment, useState} from 'react';\nimport {DeleteIcon} from '../../../../icons/material/Delete';\nimport {ConfirmationDialog} from '../../../../ui/overlays/dialog/confirmation-dialog';\nimport {IconButton} from '../../../../ui/buttons/icon-button';\nimport {MoreVertIcon} from '../../../../icons/material/MoreVert';\nimport {RestartAltIcon} from '../../../../icons/material/RestartAlt';\nimport {appearanceState, AppearanceValues} from '../../appearance-store';\nimport {toast} from '../../../../ui/toast/toast';\nimport {\n Menu,\n MenuItem,\n MenuTrigger,\n} from '../../../../ui/navigation/menu/menu-trigger';\nimport {DialogTrigger} from '../../../../ui/overlays/dialog/dialog-trigger';\nimport {message} from '../../../../i18n/message';\nimport {Trans} from '../../../../i18n/trans';\nimport {useNavigate} from '../../../../utils/hooks/use-navigate';\nimport {useFieldArray, useFormContext} from 'react-hook-form';\nimport {useParams} from 'react-router-dom';\n\nexport function ThemeMoreOptionsButton() {\n const navigate = useNavigate();\n const {themeIndex} = useParams();\n const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);\n const {setValue, getValues} = useFormContext();\n const {fields, remove} = useFieldArray({\n name: 'appearance.themes.all',\n });\n\n const deleteTheme = () => {\n if (fields.length <= 1) {\n toast.danger(message('At least one theme is required'));\n return;\n }\n if (themeIndex) {\n navigate('/admin/appearance/themes');\n remove(+themeIndex);\n setValue('appearance.themes.selectedThemeId', null);\n }\n };\n\n return (\n \n {\n if (key === 'delete') {\n setConfirmDialogOpen(true);\n } else if (key === 'reset') {\n const path =\n `appearance.themes.all.${+themeIndex!}` as 'appearance.themes.all.0';\n const defaultColors = getValues(`${path}.is_dark`)\n ? appearanceState().defaults!.appearance.themes.dark\n : appearanceState().defaults!.appearance.themes.light;\n\n Object.entries(defaultColors).forEach(([colorName, themeValue]) => {\n appearanceState().preview.setThemeValue(colorName, themeValue);\n });\n appearanceState().preview.setThemeFont(null);\n\n setValue(`${path}.values`, defaultColors, {\n shouldDirty: true,\n });\n setValue(`${path}.font`, undefined, {\n shouldDirty: true,\n });\n }\n }}\n >\n \n \n \n \n }>\n \n \n }>\n \n \n \n \n {\n if (isConfirmed) {\n deleteTheme();\n }\n setConfirmDialogOpen(false);\n }}\n >\n }\n body={}\n confirm={}\n />\n \n \n );\n}\n","import {message} from '@common/i18n/message';\nimport {useParams} from 'react-router-dom';\nimport {useFormContext} from 'react-hook-form';\nimport {AppearanceValues} from '@common/admin/appearance/appearance-store';\nimport {Menu, MenuTrigger} from '@common/ui/navigation/menu/menu-trigger';\nimport {AppearanceButton} from '@common/admin/appearance/appearance-button';\nimport {ColorIcon} from '@common/admin/appearance/sections/themes/color-icon';\nimport clsx from 'clsx';\nimport {Trans} from '@common/i18n/trans';\nimport {Item} from '@common/ui/forms/listbox/item';\n\nconst navbarColorMap = [\n {\n label: message('Accent'),\n value: 'primary',\n bgColor: 'bg-primary',\n previewBgColor: 'text-primary',\n },\n {\n label: message('Background'),\n value: 'bg',\n bgColor: 'bg-background',\n previewBgColor: 'text-background',\n },\n {\n label: message('Background alt'),\n value: 'bg-alt',\n bgColor: 'bg-alt',\n previewBgColor: 'text-background-alt',\n },\n {\n label: message('Transparent'),\n value: 'transparent',\n bgColor: 'bg-transparent',\n previewBgColor: 'text-transparent',\n },\n];\n\nexport function NavbarColorPicker() {\n const {themeIndex} = useParams();\n const {watch, setValue} = useFormContext();\n const key =\n `appearance.themes.all.${themeIndex!}.values.--be-navbar-color` as 'appearance.themes.all.1.values.--be-navbar-color';\n const selectedValue = watch(key);\n const previewColor = navbarColorMap.find(({value}) => value === selectedValue)\n ?.previewBgColor;\n return (\n {\n setValue(key, value as string, {shouldDirty: true});\n }}\n >\n \n }\n >\n \n \n \n {navbarColorMap.map(({label, value, bgColor}) => (\n \n }\n >\n \n \n ))}\n \n \n );\n}\n","import {parseColor} from '@react-stately/color';\n\nexport function themeValueToHex(value: string): string {\n try {\n return parseColor(`rgb(${value.split(' ').join(',')})`).toString('hex');\n } catch (e) {\n return value;\n }\n}\n","import {Link, useNavigate, useParams} from 'react-router-dom';\nimport {Fragment, ReactNode, useEffect, useState} from 'react';\nimport {\n appearanceState,\n AppearanceValues,\n} from '@common/admin/appearance/appearance-store';\nimport {AppearanceButton} from '@common/admin/appearance/appearance-button';\nimport {ColorIcon} from '@common/admin/appearance/sections/themes/color-icon';\nimport {CssTheme} from '@common/ui/themes/css-theme';\nimport {colorToThemeValue} from '@common/ui/themes/utils/color-to-theme-value';\nimport {ThemeSettingsDialogTrigger} from '@common/admin/appearance/sections/themes/theme-settings-dialog-trigger';\nimport {ThemeMoreOptionsButton} from '@common/admin/appearance/sections/themes/theme-more-options-button';\nimport {ColorPickerDialog} from '@common/ui/color-picker/color-picker-dialog';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {useFormContext} from 'react-hook-form';\nimport {Trans} from '@common/i18n/trans';\nimport {NavbarColorPicker} from '@common/admin/appearance/sections/themes/navbar-color-picker';\nimport {message} from '@common/i18n/message';\nimport {themeValueToHex} from '@common/ui/themes/utils/theme-value-to-hex';\n\nconst colorList = [\n {\n label: message('Background'),\n key: '--be-background',\n },\n {\n label: message('Background alt'),\n key: '--be-background-alt',\n },\n {\n label: message('Foreground'),\n key: '--be-foreground-base',\n },\n {\n label: message('Accent light'),\n key: '--be-primary-light',\n },\n {\n label: message('Accent'),\n key: '--be-primary',\n },\n {\n label: message('Accent dark'),\n key: '--be-primary-dark',\n },\n {\n label: message('Text on accent'),\n key: '--be-on-primary',\n },\n {\n label: message('Chip'),\n key: '--be-background-chip',\n },\n];\n\nexport function ThemeEditor() {\n const navigate = useNavigate();\n const {themeIndex} = useParams();\n const {getValues, watch} = useFormContext();\n\n const theme = getValues(`appearance.themes.all.${+themeIndex!}`);\n const selectedFont = watch(\n `appearance.themes.all.${+themeIndex!}.font.family`,\n );\n\n // go to theme list, if theme can't be found\n useEffect(() => {\n if (!theme) {\n navigate('/admin/appearance/themes');\n }\n }, [navigate, theme]);\n\n // select theme in preview on initial render\n useEffect(() => {\n if (theme?.id) {\n appearanceState().preview.setActiveTheme(theme.id);\n }\n }, [theme?.id]);\n\n if (!theme) return null;\n\n return (\n \n
\n \n \n
\n
\n }\n >\n \n \n \n \n \n
\n \n
\n \n {colorList.map(color => (\n }\n initialThemeValue={theme.values[color.key]}\n theme={theme}\n />\n ))}\n
\n
\n );\n}\n\ninterface ColorPickerTriggerProps {\n label: ReactNode;\n theme: CssTheme;\n colorName: string;\n initialThemeValue: string;\n}\nfunction ColorPickerTrigger({\n label,\n theme,\n colorName,\n initialThemeValue,\n}: ColorPickerTriggerProps) {\n const {setValue} = useFormContext();\n const {themeIndex} = useParams();\n const [selectedThemeValue, setSelectedThemeValue] =\n useState(initialThemeValue);\n\n // set color as css variable in preview and on button preview, but not in appearance values\n // this way color change can be canceled when color picker is closed and applied explicitly via apply button\n const selectThemeValue = (themeValue: string) => {\n setSelectedThemeValue(themeValue);\n appearanceState().preview.setThemeValue(colorName, themeValue);\n };\n\n useEffect(() => {\n // need to update the color here so changes via \"reset colors\" button are reflected\n setSelectedThemeValue(initialThemeValue);\n }, [initialThemeValue]);\n\n return (\n {\n selectThemeValue(colorToThemeValue(newColor));\n }}\n onClose={(newColor, {valueChanged, initialValue}) => {\n if (newColor && valueChanged) {\n setValue(\n `appearance.themes.all.${+themeIndex!}.values.${colorName}`,\n colorToThemeValue(newColor),\n {shouldDirty: true},\n );\n setValue('appearance.themes.selectedThemeId', theme.id);\n } else {\n // reset to initial value, if apply button was not clicked\n selectThemeValue(initialValue);\n }\n }}\n >\n \n }\n >\n {label}\n \n \n \n );\n}\n","import {useController} from 'react-hook-form';\nimport React, {useMemo} from 'react';\nimport {mergeProps} from '@react-aria/utils';\nimport {\n ChipField,\n ChipValue,\n} from '../../ui/forms/input-field/chip-field/chip-field';\nimport {FormChipFieldProps} from '../../ui/forms/input-field/chip-field/form-chip-field';\n\nexport function JsonChipField({children, ...props}: FormChipFieldProps) {\n const {\n field: {onChange, onBlur, value = [], ref},\n fieldState: {invalid, error},\n } = useController({\n name: props.name,\n });\n\n const arrayValue = useMemo(() => {\n const mixedValue = value as string | string[];\n return typeof mixedValue === 'string' ? JSON.parse(mixedValue) : mixedValue;\n }, [value]);\n\n const formProps: Partial> = {\n onChange: newValue => {\n const jsonValue = JSON.stringify(newValue.map(chip => chip.name));\n onChange(jsonValue);\n },\n onBlur,\n value: arrayValue,\n invalid,\n errorMessage: error?.message,\n };\n\n return ;\n}\n","import {Trans} from '@common/i18n/trans';\nimport {FormSelect} from '@common/ui/forms/select/select';\nimport {Item} from '@common/ui/forms/listbox/item';\nimport {SettingsPanel} from '@common/admin/settings/settings-panel';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {JsonChipField} from '@common/admin/settings/json-chip-field';\nimport {useTrans} from '@common/i18n/use-trans';\n\nexport function VideoSettings() {\n const {trans} = useTrans();\n return (\n }\n description={\n \n }\n >\n \n \n \n }\n >\n \n \n \n }\n >\n \n \n \n }\n >\n \n \n }\n name=\"client.streaming.qualities\"\n placeholder={trans({message: 'Add another...'})}\n />\n \n );\n}\n\nfunction SortingMethodSelect() {\n return (\n }\n selectionMode=\"single\"\n description={\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\nfunction ShownVideoTypeSelect() {\n return (\n }\n selectionMode=\"single\"\n description={\n \n }\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n","import React, {\n Children,\n cloneElement,\n ComponentPropsWithoutRef,\n isValidElement,\n ReactElement,\n ReactNode,\n useContext,\n useRef,\n useState,\n} from 'react';\nimport clsx from 'clsx';\nimport {useLayoutEffect} from '@react-aria/utils';\nimport {getFocusableTreeWalker} from '@react-aria/focus';\nimport {TabContext} from './tabs-context';\n\nexport interface TabPanelsProps {\n children: ReactNode;\n className?: string;\n}\nexport function TabPanels({children, className}: TabPanelsProps) {\n const {selectedTab, isLazy} = useContext(TabContext);\n\n // filter out falsy values, in case of conditional rendering\n const panelArray = Children.toArray(children).filter(p => !!p);\n\n let rendered: ReactNode;\n if (isLazy) {\n const el = panelArray[selectedTab] as ReactElement;\n rendered = isValidElement(el)\n ? cloneElement(panelArray[selectedTab] as ReactElement, {\n index: selectedTab,\n })\n : null;\n } else {\n rendered = panelArray.map((panel, index) => {\n if (isValidElement(panel)) {\n const isSelected = index === selectedTab;\n return cloneElement(panel, {\n index,\n 'aria-hidden': !isSelected,\n className: !isSelected\n ? clsx(panel.props.className, 'hidden')\n : panel.props.className,\n });\n }\n return null;\n });\n }\n\n return
{rendered}
;\n}\n\ninterface TabPanelProps extends ComponentPropsWithoutRef<'div'> {\n className?: string;\n children: ReactNode;\n index?: number;\n}\nexport function TabPanel({\n className,\n children,\n index,\n ...domProps\n}: TabPanelProps) {\n const {id} = useContext(TabContext);\n\n const [tabIndex, setTabIndex] = useState(0);\n const ref = useRef(null);\n\n // The tabpanel should have tabIndex=0 when there are no tabbable elements within it.\n // Otherwise, tabbing from the focused tab should go directly to the first tabbable element\n // within the tabpanel.\n useLayoutEffect(() => {\n if (ref?.current) {\n const update = () => {\n // Detect if there are any tabbable elements and update the tabIndex accordingly.\n const walker = getFocusableTreeWalker(ref.current!, {tabbable: true});\n setTabIndex(walker.nextNode() ? undefined : 0);\n };\n\n update();\n\n // Update when new elements are inserted, or the tabIndex/disabled attribute updates.\n const observer = new MutationObserver(update);\n observer.observe(ref.current, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: ['tabIndex', 'disabled'],\n });\n\n return () => {\n observer.disconnect();\n };\n }\n }, [ref]);\n\n return (\n \n {children}\n
\n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {AdminSettings} from '@common/admin/settings/admin-settings';\nimport {Fragment} from 'react';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {Trans} from '@common/i18n/trans';\nimport {FormSelect} from '@common/ui/forms/select/select';\nimport {Item} from '@common/ui/forms/listbox/item';\n\nexport function ContentSettingsGeneralPanel() {\n const {watch} = useFormContext();\n return (\n \n \n \n }\n >\n \n \n \n }\n >\n \n \n {watch('client.titles.enable_comments') && (\n \n }\n >\n \n \n )}\n \n );\n}\n\nfunction SortingMethodSelect() {\n return (\n }\n selectionMode=\"single\"\n description={\n \n }\n >\n \n \n \n \n \n \n \n );\n}\n","import {ReactNode, useEffect, useRef} from 'react';\nimport {useFormContext} from 'react-hook-form';\nimport clsx from 'clsx';\n\ninterface Props {\n children: (isInvalid: boolean) => ReactNode;\n name: string;\n separatorBottom?: boolean;\n separatorTop?: boolean;\n}\nexport function SettingsErrorGroup({\n children,\n name,\n separatorBottom = true,\n separatorTop = true,\n}: Props) {\n const {\n formState: {errors},\n } = useFormContext>();\n\n const ref = useRef(null);\n const error = errors[name];\n\n useEffect(() => {\n if (error) {\n ref.current?.scrollIntoView({behavior: 'smooth'});\n }\n }, [error]);\n\n return (\n \n {children(!!error)}\n {error && (\n \n )}\n \n );\n}\n","import {Fragment} from 'react';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {Trans} from '@common/i18n/trans';\nimport {FormSelect} from '@common/ui/forms/select/select';\nimport {Item} from '@common/ui/forms/listbox/item';\nimport {SettingsSeparator} from '@common/admin/settings/settings-separator';\nimport {useFormContext} from 'react-hook-form';\nimport {AdminSettings} from '@common/admin/settings/admin-settings';\nimport {SettingsErrorGroup} from '@common/admin/settings/settings-error-group';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {useValueLists} from '@common/http/value-lists';\n\nexport function ContentSettingsAutomationPanel() {\n const {watch} = useFormContext();\n return (\n \n \n \n }\n >\n \n \n \n }\n >\n \n \n \n \n }\n >\n \n \n {watch('client.content.people_provider') === 'tmdb' && (\n \n }\n >\n \n \n )}\n \n \n );\n}\n\nfunction SearchMethodSelect() {\n return (\n }\n description={\n \n }\n >\n \n }\n >\n \n \n \n }\n >\n \n \n \n }\n >\n \n \n \n );\n}\n\nfunction TmdbFields() {\n const {data} = useValueLists(['tmdbLanguages']);\n const {watch: w} = useFormContext();\n const shouldShow = [\n w('client.content.people_provider'),\n w('client.content.title_provider'),\n w('client.content.search_provider'),\n ].some(provider => `${provider}`.toLowerCase().includes('tmdb'));\n\n if (!shouldShow) {\n return null;\n }\n\n return (\n \n {isInvalid => (\n \n }\n className=\"mb-24\"\n required\n />\n }\n description={\n \n }\n >\n {data?.tmdbLanguages.map(({code, name}) => (\n \n {name}\n \n ))}\n \n \n \n \n \n )}\n \n );\n}\n","import {Trans} from '@common/i18n/trans';\nimport React, {Fragment, ReactNode, useRef, useState} from 'react';\nimport {DragPreviewRenderer} from '@common/ui/interactions/dnd/use-draggable';\nimport {useFormContext} from 'react-hook-form';\nimport {AdminSettingsWithFiles} from '@common/admin/settings/requests/update-admin-settings';\nimport {moveItemInNewArray} from '@common/utils/array/move-item-in-new-array';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {DragHandleIcon} from '@common/icons/material/DragHandle';\nimport {Checkbox} from '@common/ui/forms/toggle/checkbox';\nimport {DragPreview} from '@common/ui/interactions/dnd/drag-preview';\nimport clsx from 'clsx';\nimport {TitlePageSections} from '@app/titles/pages/title-page/sections/title-page-sections';\nimport {MessageDescriptor} from '@common/i18n/message-descriptor';\nimport {AdminSettings} from '@common/admin/settings/admin-settings';\nimport {useSortable} from '@common/ui/interactions/dnd/sortable/use-sortable';\n\ninterface SectionItem {\n name: (typeof TitlePageSections)[number];\n title: MessageDescriptor;\n}\n\nconst defaultItems: SectionItem[] = [\n {name: 'episodes', title: {message: 'Episode grid'}},\n {name: 'seasons', title: {message: 'Season grid'}},\n {name: 'videos', title: {message: 'Video grid'}},\n {name: 'images', title: {message: 'Image grid'}},\n {name: 'reviews', title: {message: 'Reviews'}},\n {name: 'cast', title: {message: 'Cast grid'}},\n {name: 'related', title: {message: 'Related titles'}},\n];\n\nexport function ContentSettingsTitlePagePanel() {\n const {getValues, setValue} = useFormContext();\n const getSavedValue = (): string[] => {\n return getValues('client.title_page.sections') || [];\n };\n\n const [items, setItems] = useState(() => {\n const savedValue = getSavedValue();\n const sortFn = (x: string) =>\n savedValue.includes(x) ? savedValue.indexOf(x) : savedValue.length;\n return [...defaultItems].sort((a, b) => sortFn(a.name) - sortFn(b.name));\n });\n\n return (\n
\n
\n \n
\n \n
\n
\n {items.map((section, index) => (\n }\n onToggle={(section, checked) => {\n const savedValue = getSavedValue();\n const newValue = checked\n ? [...savedValue, section.name]\n : savedValue.filter(x => x !== section.name);\n setValue('client.title_page.sections', newValue as any);\n }}\n onSortEnd={(oldIndex, newIndex) => {\n const sortedItems = moveItemInNewArray(items, oldIndex, newIndex);\n setItems(sortedItems);\n const savedValue = getSavedValue();\n const newValue = sortedItems\n .filter(x => savedValue.includes(x.name))\n .map(x => x.name);\n setValue('client.title_page.sections', newValue);\n }}\n />\n ))}\n
\n );\n}\n\ninterface ListItemLayoutProps {\n isFirst: boolean;\n items: SectionItem[];\n section: SectionItem;\n title: ReactNode;\n onSortEnd: (oldIndex: number, newIndex: number) => void;\n onToggle: (section: SectionItem, checked: boolean) => void;\n}\nfunction ListItemLayout({\n isFirst,\n title,\n items,\n section,\n onSortEnd,\n onToggle,\n}: ListItemLayoutProps) {\n const ref = useRef(null);\n const previewRef = useRef(null);\n const {watch} = useFormContext();\n\n const savedValue = watch('client.title_page.sections') || [];\n const isChecked = savedValue.includes(section.name);\n\n const {sortableProps, dragHandleRef} = useSortable({\n ref,\n item: section,\n items,\n type: 'titlePageSections',\n preview: previewRef,\n strategy: 'line',\n onSortEnd,\n });\n\n return (\n \n \n \n \n \n
\n
{title}
\n
\n {\n onToggle(section, !isChecked);\n }}\n />\n \n \n
\n );\n}\n\ninterface DragPreviewProps {\n title: ReactNode;\n}\nconst TabDragPreview = React.forwardRef(\n ({title}, ref) => {\n return (\n \n {() => (\n
{title}
\n )}\n
\n );\n },\n);\n","import {Trans} from '@common/i18n/trans';\nimport {SettingsPanel} from '@common/admin/settings/settings-panel';\nimport {Tabs} from '@common/ui/tabs/tabs';\nimport {TabList} from '@common/ui/tabs/tab-list';\nimport {Tab} from '@common/ui/tabs/tab';\nimport {TabPanel, TabPanels} from '@common/ui/tabs/tab-panels';\nimport {ContentSettingsGeneralPanel} from '@app/admin/settings/content-settings/content-settings-general-panel';\nimport {ContentSettingsAutomationPanel} from '@app/admin/settings/content-settings/content-settings-automation-panel';\nimport {ContentSettingsTitlePagePanel} from '@app/admin/settings/content-settings/content-settings-title-page-panel';\n\nexport function ContentSettings() {\n return (\n }\n description={\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 {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient} from '@common/http/query-client';\nimport {useQuery} from '@tanstack/react-query';\n\ninterface Response extends BackendResponse {\n models: {model: string; name: string}[];\n}\n\nexport function useSearchModels() {\n return useQuery({\n queryKey: ['search-models'],\n queryFn: () => fetchModels(),\n });\n}\n\nfunction fetchModels(): Promise {\n return apiClient.get('admin/search/models').then(response => response.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient} from '@common/http/query-client';\nimport {toast} from '@common/ui/toast/toast';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {message} from '@common/i18n/message';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Payload {\n model: string;\n driver: string;\n}\n\nexport function useImportSearchModels() {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: (payload: Payload) => importModels(payload),\n onSuccess: () => {\n toast(trans(message('Imported search models')));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction importModels(payload: Payload): Promise {\n return apiClient.post('admin/search/import', payload).then(r => r.data);\n}\n","import {FormSelect, Select} from '@common/ui/forms/select/select';\nimport {SettingsPanel} from '../../settings-panel';\nimport {Trans} from '@common/i18n/trans';\nimport {useFormContext} from 'react-hook-form';\nimport {AdminSettingsWithFiles} from '@common/admin/settings/requests/update-admin-settings';\nimport {Item} from '@common/ui/forms/listbox/item';\nimport {SectionHelper} from '@common/ui/section-helper';\nimport {SettingsErrorGroup} from '@common/admin/settings/settings-error-group';\nimport {Fragment, useState} from 'react';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {useSearchModels} from '@common/admin/settings/pages/search-settings/requests/use-search-models';\nimport {Button} from '@common/ui/buttons/button';\nimport {useImportSearchModels} from '@common/admin/settings/pages/search-settings/requests/use-import-search-models';\n\nexport function SearchSettings() {\n return (\n }\n description={\n \n }\n >\n \n \n \n );\n}\n\nfunction SearchMethodSelect() {\n const {watch} = useFormContext();\n const selectedMethod = watch('server.scout_driver');\n\n return (\n \n {isInvalid => (\n \n }\n description={\n \n }\n >\n Mysql\n Meilisearch\n TNTSearch\n \n Elasticsearch\n \n Algolia\n \n {selectedMethod === 'mysql' && }\n {selectedMethod === 'meilisearch' && }\n {selectedMethod === 'algolia' && }\n {selectedMethod ===\n 'Matchish\\\\ScoutElasticSearch\\\\Engines\\\\ElasticSearchEngine' && (\n \n )}\n \n )}\n \n );\n}\n\nfunction MysqlFields() {\n const {clearErrors} = useFormContext();\n return (\n }\n onSelectionChange={() => {\n clearErrors();\n }}\n >\n \n \n \n \n \n \n \n \n \n \n );\n}\n\nfunction MeilisearchFields() {\n return (\n }\n description={\n Meilisearch needs to be installed and running for this method to work.\"\n values={{\n a: parts => (\n \n {parts}\n \n ),\n }}\n />\n }\n />\n );\n}\n\nfunction ElasticsearchField() {\n return (\n }\n description={\n Elasticsearch needs to be installed and running for this method to work.\"\n values={{\n a: parts => (\n \n {parts}\n \n ),\n }}\n />\n }\n />\n );\n}\n\nfunction AlgoliaFields() {\n return (\n \n }\n required\n />\n }\n required\n />\n \n );\n}\n\nfunction ImportRecordsPanel() {\n const {getValues} = useFormContext();\n const {data} = useSearchModels();\n const importModels = useImportSearchModels();\n const [selectedModel, setSelectedModel] = useState('*');\n return (\n }\n description={\n \n \n
\n
\n \n
\n }\n actions={\n
\n }\n selectedValue={selectedModel}\n onSelectionChange={newValue => {\n setSelectedModel(newValue as string);\n }}\n >\n \n \n \n {data?.models.map(item => (\n \n \n \n ))}\n \n {\n importModels.mutate({\n model: selectedModel,\n driver: getValues('server.scout_driver')!,\n });\n }}\n >\n \n \n
\n }\n />\n );\n}\n","import {RouteObject} from 'react-router-dom';\nimport {VideoSettings} from '@app/admin/settings/video-settings';\nimport {ContentSettings} from '@app/admin/settings/content-settings/content-settings';\nimport {SearchSettings} from '@common/admin/settings/pages/search-settings/search-settings';\n\nexport const AppSettingsRoutes: RouteObject[] = [\n {\n path: 'search',\n element: ,\n },\n {\n path: 'videos',\n element: ,\n },\n {\n path: 'content',\n element: ,\n },\n];\n","import {useFormContext} from 'react-hook-form';\nimport {SettingsPanel} from '../settings-panel';\nimport {FormSwitch} from '../../../ui/forms/toggle/switch';\nimport {SettingsSeparator} from '../settings-separator';\nimport {LearnMoreLink} from '../learn-more-link';\nimport {AdminSettings} from '../admin-settings';\nimport {FormTextField} from '../../../ui/forms/input-field/text-field/text-field';\nimport {SettingsErrorGroup} from '../settings-error-group';\nimport {JsonChipField} from '../json-chip-field';\nimport {Tabs} from '../../../ui/tabs/tabs';\nimport {TabList} from '../../../ui/tabs/tab-list';\nimport {Tab} from '../../../ui/tabs/tab';\nimport {TabPanel, TabPanels} from '../../../ui/tabs/tab-panels';\nimport {Trans} from '../../../i18n/trans';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {Fragment} from 'react';\n\nexport function SubscriptionSettings() {\n const {trans} = useTrans();\n return (\n }\n description={\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 name=\"client.billing.accepted_cards\"\n placeholder={trans({message: 'Add new card...'})}\n />\n \n \n }\n name=\"client.billing.invoice.address\"\n className=\"mb-30\"\n />\n }\n description={\n \n }\n name=\"client.billing.invoice.notes\"\n />\n \n \n \n \n );\n}\n\nfunction PaypalSection() {\n const {watch} = useFormContext();\n const paypalIsEnabled = watch('client.billing.paypal.enable');\n return (\n
\n \n \n \n
\n }\n >\n \n \n {paypalIsEnabled ? (\n \n {isInvalid => (\n \n }\n required\n invalid={isInvalid}\n className=\"mb-20\"\n />\n }\n required\n invalid={isInvalid}\n className=\"mb-20\"\n />\n }\n required\n invalid={isInvalid}\n className=\"mb-20\"\n />\n \n \n \n }\n >\n \n \n \n )}\n \n ) : null}\n \n );\n}\n\nfunction StripeSection() {\n const {watch} = useFormContext();\n const stripeEnabled = watch('client.billing.stripe.enable');\n return (\n \n \n \n \n \n }\n >\n \n \n {stripeEnabled ? (\n \n {isInvalid => (\n \n }\n required\n className=\"mb-20\"\n invalid={isInvalid}\n />\n }\n required\n className=\"mb-20\"\n invalid={isInvalid}\n />\n }\n className=\"mb-20\"\n invalid={isInvalid}\n />\n \n )}\n \n ) : null}\n \n );\n}\n","import {FormSelect, Option} from '../../../ui/forms/select/select';\nimport {SettingsPanel} from '../settings-panel';\nimport {useValueLists} from '../../../http/value-lists';\nimport {Section} from '../../../ui/forms/listbox/section';\nimport {FormRadio} from '../../../ui/forms/radio-group/radio';\nimport {FormRadioGroup} from '../../../ui/forms/radio-group/radio-group';\nimport {DateFormatPresets, FormattedDate} from '../../../i18n/formatted-date';\nimport {FormSwitch} from '../../../ui/forms/toggle/switch';\nimport {Trans} from '../../../i18n/trans';\nimport {useCurrentDateTime} from '../../../i18n/use-current-date-time';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {message} from '@common/i18n/message';\n\nexport function LocalizationSettings() {\n const {data} = useValueLists(['timezones', 'localizations']);\n const today = useCurrentDateTime();\n const {trans} = useTrans();\n return (\n }\n description={\n \n }\n >\n }\n searchPlaceholder={trans(message('Search timezones'))}\n description={\n \n }\n >\n \n {Object.entries(data?.timezones || {}).map(([groupName, timezones]) => (\n
\n {timezones.map(timezone => (\n \n ))}\n
\n ))}\n \n }\n description={\n \n }\n >\n \n {(data?.localizations || []).map(locale => (\n \n ))}\n \n }\n description={\n \n }\n >\n \n \n \n {Object.entries(DateFormatPresets).map(([format, options]) => (\n \n \n \n ))}\n \n \n }\n >\n \n \n \n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {SettingsPanel} from '@common/admin/settings/settings-panel';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {AdminSettings} from '@common/admin/settings/admin-settings';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {SettingsErrorGroup} from '@common/admin/settings/settings-error-group';\nimport {Trans} from '@common/i18n/trans';\nimport {Fragment} from 'react';\nimport {Link} from 'react-router-dom';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {SettingsSeparator} from '@common/admin/settings/settings-separator';\nimport {Button} from '@common/ui/buttons/button';\n\nexport function AuthenticationSettings() {\n return (\n }\n description={\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 description={\n \n }\n />\n \n );\n}\n\nexport function MailNotSetupWarning() {\n const {watch} = useFormContext();\n const mailSetup = watch('server.mail_setup');\n if (mailSetup) return null;\n\n return (\n

\n Fix now\"\n values={{\n a: text => (\n \n {text}\n \n ),\n }}\n />\n

\n );\n}\n\nfunction EmailConfirmationSection() {\n return (\n \n \n \n \n }\n >\n \n \n );\n}\n\nfunction EnvatoSection() {\n const {watch} = useFormContext();\n const settings = useSettings();\n const envatoLoginEnabled = watch('client.social.envato.enable');\n\n if (!(settings as any).envato?.enable) return null;\n\n return (\n \n {isInvalid => (\n <>\n \n }\n >\n \n \n {!!envatoLoginEnabled && (\n <>\n }\n required\n />\n }\n required\n />\n }\n required\n />\n \n )}\n \n )}\n \n );\n}\n\nfunction GoogleSection() {\n const {watch} = useFormContext();\n const googleLoginEnabled = watch('client.social.google.enable');\n\n return (\n \n {isInvalid => (\n <>\n \n }\n >\n \n \n {!!googleLoginEnabled && (\n <>\n }\n required\n />\n }\n required\n />\n \n )}\n \n )}\n \n );\n}\n\nfunction FacebookSection() {\n const {watch} = useFormContext();\n const facebookLoginEnabled = watch('client.social.facebook.enable');\n\n return (\n \n {isInvalid => (\n <>\n \n }\n >\n \n \n {!!facebookLoginEnabled && (\n <>\n }\n required\n />\n }\n required\n />\n \n )}\n \n )}\n \n );\n}\n\nfunction TwitterSection() {\n const {watch} = useFormContext();\n const twitterLoginEnabled = watch('client.social.twitter.enable');\n\n return (\n \n {isInvalid => (\n <>\n \n }\n >\n \n \n {!!twitterLoginEnabled && (\n <>\n }\n required\n />\n }\n required\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\nexport interface FetchMaxServerUploadSizeResponse extends BackendResponse {\n maxSize: string;\n}\n\nfunction fetchMaxServerUploadSize(): Promise {\n return apiClient\n .get('uploads/server-max-file-size')\n .then(response => response.data);\n}\n\nexport function useMaxServerUploadSize() {\n return useQuery({\n queryKey: ['MaxServerUploadSize'],\n queryFn: () => fetchMaxServerUploadSize(),\n });\n}\n","export const spaceUnits = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];\n","export type SpaceUnit = 'KB' | 'MB' | 'GB' | 'TB' | 'PB';\n\nexport function convertToBytes(value: number, unit: SpaceUnit): number {\n if (value == null) return 0;\n switch (unit) {\n case 'KB':\n return value * 1024;\n case 'MB':\n return value * 1024 ** 2;\n case 'GB':\n return value * 1024 ** 3;\n case 'TB':\n return value * 1024 ** 4;\n case 'PB':\n return value * 1024 ** 5;\n default:\n return value;\n }\n}\n","import {useController} from 'react-hook-form';\nimport {mergeProps} from '@react-aria/utils';\nimport React, {useEffect, useState} from 'react';\nimport memoize from 'nano-memoize';\nimport {\n FormTextFieldProps,\n TextField,\n TextFieldProps,\n} from './text-field/text-field';\nimport {prettyBytes} from '../../../uploads/utils/pretty-bytes';\nimport {Option, Select} from '../select/select';\nimport {spaceUnits} from '../../../uploads/utils/space-units';\nimport {\n convertToBytes,\n SpaceUnit,\n} from '../../../uploads/utils/convert-to-bytes';\n\n// 99TB\nconst MaxValue = 108851651149824;\n\nexport const FormFileSizeField = React.forwardRef<\n HTMLDivElement,\n FormTextFieldProps\n>(({name, ...props}, ref) => {\n const {\n field: {\n onChange: setByteValue,\n onBlur,\n value: byteValue = '',\n ref: inputRef,\n },\n fieldState: {invalid, error},\n } = useController({\n name,\n });\n\n const [liveValue, setLiveValue] = useState('');\n const [unit, setUnit] = useState('MB');\n\n useEffect(() => {\n if (byteValue == null || byteValue === '') {\n setLiveValue('');\n return;\n }\n const {amount, unit: newUnit} = fromBytes({\n bytes: Math.min(byteValue, MaxValue),\n });\n setUnit(newUnit || 'MB');\n setLiveValue(Number.isNaN(amount) ? '' : amount);\n }, [byteValue, unit]);\n\n const formProps: TextFieldProps = {\n onChange: e => {\n const value = parseInt(e.target.value);\n if (Number.isNaN(value)) {\n setByteValue(value);\n } else {\n const newBytes = convertToBytes(\n parseInt(e.target.value),\n unit as SpaceUnit\n );\n setByteValue(newBytes);\n }\n },\n onBlur,\n value: liveValue,\n invalid,\n errorMessage: error?.message,\n inputRef,\n };\n\n const unitSelect = (\n {\n const newBytes = convertToBytes(\n (liveValue || 0) as number,\n newUnit as SpaceUnit\n );\n setByteValue(newBytes);\n }}\n >\n {spaceUnits.slice(0, 5).map(u => (\n \n ))}\n \n );\n\n return (\n \n );\n});\n\nconst fromBytes = memoize(\n ({bytes}: {bytes: number}): {amount: number | string; unit: SpaceUnit} => {\n const pretty = prettyBytes(bytes);\n if (!pretty) return {amount: '', unit: 'MB'};\n let amount = parseInt(pretty.split(' ')[0]);\n // get rid of any punctuation\n amount = Math.round(amount);\n return {amount, unit: pretty.split(' ')[1] as SpaceUnit};\n }\n);\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient} from '../../../../http/query-client';\nimport {useTrans} from '../../../../i18n/use-trans';\nimport {BackendResponse} from '../../../../http/backend-response/backend-response';\nimport {showHttpErrorToast} from '../../../../utils/http/show-http-error-toast';\nimport {message} from '../../../../i18n/message';\nimport {toast} from '../../../../ui/toast/toast';\n\ninterface Response extends BackendResponse {}\n\nexport function useUploadS3Cors() {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: () => uploadCors(),\n onSuccess: () => {\n toast(trans(message('CORS file updated')));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction uploadCors(): Promise {\n return apiClient.post('s3/cors/upload').then(r => r.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient} from '../../../../../http/query-client';\nimport {BackendResponse} from '../../../../../http/backend-response/backend-response';\nimport {showHttpErrorToast} from '../../../../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {\n refreshToken: string;\n}\n\ninterface Payload {\n app_key: string;\n app_secret: string;\n access_code: string;\n}\n\nexport function useGenerateDropboxRefreshToken() {\n return useMutation({\n mutationFn: (props: Payload) => generateToken(props),\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction generateToken(payload: Payload): Promise {\n return apiClient\n .post('settings/uploading/dropbox-refresh-token', payload)\n .then(r => r.data);\n}\n","import {Fragment} from 'react';\nimport {FormTextField} from '../../../../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../../../../i18n/trans';\nimport {CredentialFormProps} from '../uploading-settings';\nimport {Button} from '../../../../../ui/buttons/button';\nimport {Dialog} from '../../../../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../../../../ui/overlays/dialog/dialog-header';\nimport {DialogBody} from '../../../../../ui/overlays/dialog/dialog-body';\nimport {useForm, useFormContext} from 'react-hook-form';\nimport {Form} from '../../../../../ui/forms/form';\nimport {DialogTrigger} from '../../../../../ui/overlays/dialog/dialog-trigger';\nimport {AdminSettings} from '../../../admin-settings';\nimport {DialogFooter} from '../../../../../ui/overlays/dialog/dialog-footer';\nimport {useDialogContext} from '../../../../../ui/overlays/dialog/dialog-context';\nimport {useGenerateDropboxRefreshToken} from './use-generate-dropbox-refresh-token';\n\nexport function DropboxForm({isInvalid}: CredentialFormProps) {\n const {watch, setValue} = useFormContext();\n const appKey = watch('server.storage_dropbox_app_key');\n const appSecret = watch('server.storage_dropbox_app_secret');\n\n return (\n \n }\n required\n />\n }\n required\n />\n }\n required\n />\n {\n if (refreshToken) {\n setValue('server.storage_dropbox_refresh_token', refreshToken);\n }\n }}\n >\n \n \n \n \n \n \n );\n}\n\ninterface DropboxRefreshTokenDialogProps {\n appKey: string;\n appSecret: string;\n}\nfunction DropboxRefreshTokenDialog({\n appKey,\n appSecret,\n}: DropboxRefreshTokenDialogProps) {\n const form = useForm<{accessCode: string}>();\n const {formId, close} = useDialogContext();\n const generateRefreshToken = useGenerateDropboxRefreshToken();\n return (\n \n \n \n \n \n {\n generateRefreshToken.mutate(\n {\n app_key: appKey,\n app_secret: appSecret,\n access_code: data.accessCode,\n },\n {\n onSuccess: response => {\n close(response.refreshToken);\n },\n },\n );\n }}\n >\n
\n
\n \n
\n \n \n \n
\n }\n required\n />\n \n
\n \n {\n close();\n }}\n >\n \n \n \n \n \n \n
\n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {SettingsPanel} from '../../settings-panel';\nimport {FormSelect, Option} from '../../../../ui/forms/select/select';\nimport {AdminSettings} from '../../admin-settings';\nimport {SettingsErrorGroup} from '../../settings-error-group';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {FormRadioGroup} from '@common/ui/forms/radio-group/radio-group';\nimport {FormRadio} from '@common/ui/forms/radio-group/radio';\nimport {SectionHelper} from '@common/ui/section-helper';\nimport {useMaxServerUploadSize} from './max-server-upload-size';\nimport {SettingsSeparator} from '../../settings-separator';\nimport {JsonChipField} from '../../json-chip-field';\nimport {FormFileSizeField} from '@common/ui/forms/input-field/file-size-field';\nimport {Trans} from '@common/i18n/trans';\nimport {Fragment} from 'react';\nimport {useUploadS3Cors} from './use-upload-s3-cors';\nimport {Button} from '@common/ui/buttons/button';\nimport {DropboxForm} from './dropbox-form/dropbox-form';\nimport {useAdminSettings} from '../../requests/use-admin-settings';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {message} from '@common/i18n/message';\n\nexport function UploadingSettings() {\n const {trans} = useTrans();\n return (\n }\n description={\n \n }\n >\n \n \n \n \n {isInvalid => (\n }\n description={\n \n }\n >\n \n \n \n \n \n \n \n \n \n \n )}\n \n }\n placeholder=\"Infinity\"\n description={\n \n }\n />\n \n \n }\n description={\n \n }\n />\n }\n description={\n \n }\n />\n }\n placeholder={trans(message('Add extension...'))}\n description={\n \n }\n />\n }\n placeholder={trans(message('Add extension...'))}\n description={\n \n }\n />\n \n );\n}\n\nfunction MaxUploadSizeSection() {\n const {data} = useMaxServerUploadSize();\n return (\n :size\"\n values={{size: data?.maxSize, b: chunks => {chunks}}}\n />\n }\n />\n );\n}\n\nfunction PrivateUploadSection() {\n const {watch, clearErrors} = useFormContext();\n const isEnabled = watch('server.uploads_disk_driver');\n\n if (!isEnabled) return null;\n\n return (\n }\n description={\n \n }\n onSelectionChange={() => {\n clearErrors();\n }}\n >\n \n \n \n \n \n \n \n \n );\n}\n\nfunction PublicUploadSection() {\n const {watch, clearErrors} = useFormContext();\n const isEnabled = watch('server.public_disk_driver');\n\n if (!isEnabled) return null;\n\n return (\n }\n selectionMode=\"single\"\n name=\"server.public_disk_driver\"\n description={\n \n }\n onSelectionChange={() => {\n clearErrors();\n }}\n >\n \n \n \n \n \n \n );\n}\n\nfunction CredentialsSection() {\n const {watch} = useFormContext();\n const drives = [\n watch('server.uploads_disk_driver'),\n watch('server.public_disk_driver'),\n ];\n\n if (drives[0] === 'local' && drives[1] === 'local') {\n return null;\n }\n\n return (\n \n {isInvalid => {\n if (drives.includes('s3')) {\n return ;\n }\n if (drives.includes('ftp')) {\n return ;\n }\n if (drives.includes('dropbox')) {\n return ;\n }\n if (drives.includes('digitalocean_s3')) {\n return ;\n }\n if (drives.includes('backblaze_s3')) {\n return ;\n }\n }}\n \n );\n}\n\nexport interface CredentialFormProps {\n isInvalid: boolean;\n}\nfunction S3Form({isInvalid}: CredentialFormProps) {\n return (\n \n }\n required\n />\n }\n required\n />\n }\n pattern=\"[a-z1-9\\-]+\"\n placeholder=\"us-east-1\"\n />\n }\n required\n />\n }\n description={\n \n }\n />\n \n \n );\n}\n\nfunction DigitalOceanForm({isInvalid}: CredentialFormProps) {\n return (\n \n }\n required\n />\n }\n required\n />\n }\n pattern=\"[a-z0-9\\-]+\"\n placeholder=\"us-east-1\"\n required\n />\n }\n required\n />\n \n \n );\n}\n\nfunction BackblazeForm({isInvalid}: CredentialFormProps) {\n return (\n \n }\n required\n />\n }\n required\n />\n }\n pattern=\"[a-z0-9\\-]+\"\n placeholder=\"us-west-002\"\n required\n />\n }\n required\n />\n \n \n );\n}\n\ninterface S3DirectUploadFieldProps {\n invalid: boolean;\n}\nfunction S3DirectUploadField({invalid}: S3DirectUploadFieldProps) {\n const uploadCors = useUploadS3Cors();\n const {data: defaultSettings} = useAdminSettings();\n\n const s3DriverEnabled =\n defaultSettings?.server.uploads_disk_driver?.endsWith('s3') ||\n defaultSettings?.server.public_disk_driver?.endsWith('s3');\n\n return (\n \n \n

\n \n

\n

\n \n

\n \n }\n >\n \n \n {\n uploadCors.mutate();\n }}\n disabled={!s3DriverEnabled || uploadCors.isPending}\n >\n \n \n
\n );\n}\n\nfunction FtpForm({isInvalid}: CredentialFormProps) {\n return (\n <>\n }\n required\n />\n }\n required\n />\n }\n type=\"password\"\n required\n />\n }\n placeholder=\"/\"\n />\n }\n type=\"number\"\n min={0}\n placeholder=\"21\"\n />\n \n \n \n \n \n \n \n );\n}\n","import {Fragment} from 'react';\nimport {FormTextField} from '../../../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../../../i18n/trans';\n\nexport interface MailgunCredentialsProps {\n isInvalid: boolean;\n}\nexport function MailgunCredentials({isInvalid}: MailgunCredentialsProps) {\n return (\n \n }\n description={\n \n }\n required\n />\n }\n description={}\n required\n />\n }\n description={\n \n }\n placeholder=\"api.eu.mailgun.net\"\n />\n \n );\n}\n","import {FormTextField} from '../../../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../../../i18n/trans';\nimport {FormSelect} from '@common/ui/forms/select/select';\nimport {Item} from '@common/ui/forms/listbox/item';\n\nexport interface SmtpCredentialsProps {\n isInvalid: boolean;\n}\nexport function SmtpCredentials({isInvalid}: SmtpCredentialsProps) {\n return (\n <>\n }\n required\n />\n }\n required\n />\n }\n required\n />\n }\n />\n }\n >\n \n \n \n \n \n \n \n \n );\n}\n","import {FormTextField} from '../../../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../../../i18n/trans';\nimport {Fragment} from 'react';\n\nexport interface SesCredentialsProps {\n isInvalid: boolean;\n}\nexport function SesCredentials({isInvalid}: SesCredentialsProps) {\n return (\n \n }\n required\n />\n }\n required\n />\n }\n placeholder=\"us-east-1\"\n required\n />\n \n );\n}\n","import {FormTextField} from '../../../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../../../i18n/trans';\n\nexport interface PostmarkCredentialsProps {\n isInvalid: boolean;\n}\nexport function PostmarkCredentials({isInvalid}: PostmarkCredentialsProps) {\n return (\n }\n required\n />\n );\n}\n","import {createSvgIcon} from '../../../../icons/create-svg-icon';\n\nexport const GmailIcon = createSvgIcon(\n [\n ,\n ,\n ,\n ,\n ,\n ],\n 'Gmail',\n '0 0 48 48'\n);\n","import {useFormContext} from 'react-hook-form';\nimport {AdminSettings} from '../../admin-settings';\nimport {useSocialLogin} from '../../../../auth/requests/use-social-login';\nimport {toast} from '../../../../ui/toast/toast';\nimport {message} from '../../../../i18n/message';\nimport {Button} from '../../../../ui/buttons/button';\nimport {GmailIcon} from './gmail-icon';\nimport {Trans} from '../../../../i18n/trans';\nimport {Fragment} from 'react';\n\nexport function ConnectGmailPanel() {\n const {watch, setValue} = useFormContext();\n const {connectSocial} = useSocialLogin();\n const connectedEmail = watch('server.connectedGmailAccount');\n\n const handleGmailConnect = async () => {\n const e = await connectSocial('secure/settings/mail/gmail/connect');\n if (e?.status === 'SUCCESS') {\n const email = (e.callbackData as any).profile.email;\n setValue('server.connectedGmailAccount', email);\n toast(message('Connected gmail account: :email', {values: {email}}));\n }\n };\n\n const connectButton = (\n }\n onClick={() => {\n handleGmailConnect();\n }}\n >\n \n \n );\n\n const reconnectPanel = (\n
\n \n {connectedEmail}\n {\n handleGmailConnect();\n }}\n >\n \n \n
\n );\n\n return (\n \n
\n \n
\n {connectedEmail ? reconnectPanel : connectButton}\n
\n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {AdminSettings} from '../../admin-settings';\nimport {ComponentType, Fragment} from 'react';\nimport {MailgunCredentials} from './mailgun-credentials';\nimport {SmtpCredentials} from './smtp-credentials';\nimport {SesCredentials} from './ses-credentials';\nimport {PostmarkCredentials} from './postmark-credentials';\nimport {ConnectGmailPanel} from './connect-gmail-panel';\nimport {SettingsErrorGroup} from '../../settings-error-group';\nimport {FormSelect, Option} from '../../../../ui/forms/select/select';\nimport {Trans} from '../../../../i18n/trans';\nimport {LearnMoreLink} from '../../learn-more-link';\n\nexport function OutgoingMailGroup() {\n const {watch, clearErrors} = useFormContext();\n\n const selectedDriver = watch('server.mail_driver');\n const credentialForms: ComponentType<{isInvalid: boolean}>[] = [];\n\n if (selectedDriver === 'mailgun') {\n credentialForms.push(MailgunCredentials);\n }\n if (selectedDriver === 'smtp') {\n credentialForms.push(SmtpCredentials);\n }\n if (selectedDriver === 'ses') {\n credentialForms.push(SesCredentials);\n }\n if (selectedDriver === 'postmark') {\n credentialForms.push(PostmarkCredentials);\n }\n if (selectedDriver === 'gmailApi') {\n credentialForms.push(ConnectGmailPanel);\n }\n\n return (\n \n {isInvalid => (\n \n {\n clearErrors();\n }}\n invalid={isInvalid}\n selectionMode=\"single\"\n name=\"server.mail_driver\"\n label={}\n description={\n
\n \n \n
\n }\n >\n \n \n \n \n \n \n \n \n {credentialForms.length ? (\n
\n {credentialForms.map((CredentialsForm, index) => (\n \n ))}\n
\n ) : null}\n
\n )}\n \n );\n}\n","import {SettingsPanel} from '../../settings-panel';\nimport {FormTextField} from '../../../../ui/forms/input-field/text-field/text-field';\nimport {ExternalLink} from '../../../../ui/buttons/external-link';\nimport {SectionHelper} from '../../../../ui/section-helper';\nimport {SettingsSeparator} from '../../settings-separator';\nimport {Trans} from '../../../../i18n/trans';\nimport {OutgoingMailGroup} from './outgoing-mail-group';\nimport {useSettings} from '../../../../core/settings/use-settings';\n\nexport function OutgoingEmailSettings() {\n return (\n }\n description={\n \n }\n >\n }\n description={\n \n }\n required\n />\n \n }\n description={\n \n }\n required\n />\n \n }\n />\n \n \n \n );\n}\n\nfunction ContactAddressSection() {\n const {base_url} = useSettings();\n const contactPageUrl = `${base_url}/contact`;\n const link = (\n {contactPageUrl}\n );\n return (\n }\n description={\n \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 {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\nfunction clearCache(): Promise {\n return apiClient.post('cache/flush').then(r => r.data);\n}\n\nexport function useClearCache() {\n return useMutation({\n mutationFn: () => clearCache(),\n onSuccess: () => {\n toast(message('Cache cleared'));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n","import {useFormContext} from 'react-hook-form';\nimport {ComponentType} from 'react';\nimport {SettingsPanel} from '../../settings-panel';\nimport {FormSelect, Option} from '../../../../ui/forms/select/select';\nimport {SettingsErrorGroup} from '../../settings-error-group';\nimport {FormTextField} from '../../../../ui/forms/input-field/text-field/text-field';\nimport {AdminSettings} from '../../admin-settings';\nimport {useClearCache} from './clear-cache';\nimport {Button} from '../../../../ui/buttons/button';\nimport {SectionHelper} from '../../../../ui/section-helper';\nimport {Trans} from '../../../../i18n/trans';\n\nexport function CacheSettings() {\n const clearCache = useClearCache();\n return (\n }\n description={\n \n }\n >\n \n {\n clearCache.mutate();\n }}\n >\n \n \n \n }\n />\n \n );\n}\n\nfunction CacheSelect() {\n const {watch, clearErrors} = useFormContext();\n const cacheDriver = watch('server.cache_driver');\n\n let CredentialSection: ComponentType | null = null;\n if (cacheDriver === 'memcached') {\n CredentialSection = MemcachedCredentials;\n }\n\n return (\n \n {isInvalid => {\n return (\n <>\n {\n clearErrors();\n }}\n selectionMode=\"single\"\n name=\"server.cache_driver\"\n label={}\n description={\n \n }\n >\n \n \n \n \n \n \n {CredentialSection && (\n
\n \n
\n )}\n \n );\n }}\n
\n );\n}\n\ninterface CredentialProps {\n isInvalid: boolean;\n}\nfunction MemcachedCredentials({isInvalid}: CredentialProps) {\n return (\n <>\n }\n required\n />\n }\n required\n />\n \n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {SettingsPanel} from '@common/admin/settings/settings-panel';\nimport {SettingsErrorGroup} from '@common/admin/settings/settings-error-group';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {SectionHelper} from '@common/ui/section-helper';\nimport {ExternalLink} from '@common/ui/buttons/external-link';\nimport {Trans} from '@common/i18n/trans';\n\nexport function LoggingSettings() {\n return (\n }\n description={\n \n }\n >\n \n (\n {parts}\n ),\n }}\n message=\"Sentry integration provides real-time error tracking and helps identify and fix issues when site is in production.\"\n />\n }\n />\n \n );\n}\n\nfunction SentrySection() {\n const {clearErrors} = useFormContext();\n return (\n \n {isInvalid => {\n return (\n {\n clearErrors();\n }}\n invalid={isInvalid}\n name=\"server.sentry_dsn\"\n type=\"url\"\n minLength={30}\n label={}\n />\n );\n }}\n \n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {ComponentType} from 'react';\nimport {SettingsPanel} from '../settings-panel';\nimport {SettingsErrorGroup} from '../settings-error-group';\nimport {SectionHelper} from '../../../ui/section-helper';\nimport {AdminSettings} from '../admin-settings';\nimport {FormSelect, Option} from '../../../ui/forms/select/select';\nimport {FormTextField} from '../../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../../i18n/trans';\n\nexport function QueueSettings() {\n return (\n }\n description={\n \n }\n >\n \n }\n />\n \n }\n />\n \n \n );\n}\n\nfunction DriverSection() {\n const {watch, clearErrors} = useFormContext();\n const queueDriver = watch('server.queue_driver');\n\n let CredentialSection: ComponentType | null = null;\n if (queueDriver === 'sqs') {\n CredentialSection = SqsCredentials;\n }\n return (\n \n {isInvalid => {\n return (\n <>\n {\n clearErrors();\n }}\n selectionMode=\"single\"\n name=\"server.queue_driver\"\n label={}\n required\n >\n \n \n \n \n \n \n {CredentialSection && (\n
\n \n
\n )}\n \n );\n }}\n \n );\n}\n\ninterface CredentialProps {\n isInvalid: boolean;\n}\nfunction SqsCredentials({isInvalid}: CredentialProps) {\n return (\n <>\n }\n required\n />\n }\n required\n />\n }\n required\n />\n }\n required\n />\n }\n required\n />\n \n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {useContext} from 'react';\nimport {SettingsPanel} from '../settings-panel';\nimport {SettingsErrorGroup} from '../settings-error-group';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\nimport {Trans} from '@common/i18n/trans';\n\nexport function RecaptchaSettings() {\n const {settings} = useContext(SiteConfigContext);\n return (\n }\n description={\n \n }\n >\n {settings?.showRecaptchaLinkSwitch && (\n \n }\n >\n \n \n )}\n \n }\n >\n \n \n \n }\n >\n \n \n \n \n );\n}\n\nfunction RecaptchaSection() {\n const {clearErrors} = useFormContext();\n return (\n \n {isInvalid => {\n return (\n <>\n {\n clearErrors();\n }}\n invalid={isInvalid}\n name=\"client.recaptcha.site_key\"\n label={}\n />\n {\n clearErrors();\n }}\n invalid={isInvalid}\n name=\"client.recaptcha.secret_key\"\n label={}\n />\n \n );\n }}\n \n );\n}\n","import React, {ChangeEventHandler} from 'react';\nimport {mergeProps, useObjectRef} from '@react-aria/utils';\nimport {useController} from 'react-hook-form';\nimport clsx from 'clsx';\nimport {BaseFieldProps} from './base-field-props';\nimport {useField} from './use-field';\nimport {getInputFieldClassNames} from './get-input-field-class-names';\nimport {Field} from './field';\nimport {TextFieldProps} from './text-field/text-field';\n\nexport interface FileFieldProps\n extends Omit {\n onChange?: ChangeEventHandler<'input'>;\n accept?: string;\n}\nexport const FileField = React.forwardRef(\n (props, ref) => {\n const inputRef = useObjectRef(ref);\n\n const {fieldProps, inputProps} = useField({...props, focusRef: inputRef});\n\n const inputFieldClassNames = getInputFieldClassNames(props);\n\n return (\n \n \n \n );\n }\n);\n\nexport interface FormFileFieldProps extends FileFieldProps {\n name: string;\n}\nexport function FormFileField({name, ...props}: FormFileFieldProps) {\n const {\n field: {onChange, onBlur, ref},\n fieldState: {invalid, error},\n } = useController({\n name,\n });\n\n const [value, setValue] = React.useState('');\n\n const formProps: TextFieldProps = {\n onChange: e => {\n onChange(e.target.files?.[0]);\n setValue(e.target.value);\n },\n onBlur,\n value,\n invalid,\n errorMessage: error?.message,\n };\n\n return ;\n}\n","import {useFormContext} from 'react-hook-form';\nimport {SettingsPanel} from '../settings-panel';\nimport {SettingsErrorGroup} from '../settings-error-group';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {FormFileField} from '@common/ui/forms/input-field/file-field';\nimport {Trans} from '@common/i18n/trans';\nimport {Fragment} from 'react';\n\nexport function ReportsSettings() {\n return (\n }\n description={\n \n }\n >\n \n \n );\n}\n\nfunction AnalyticsSection() {\n const {clearErrors} = useFormContext();\n return (\n \n {isInvalid => (\n \n {\n clearErrors();\n }}\n invalid={isInvalid}\n name=\"files.certificate\"\n accept=\".json\"\n label={}\n />\n {\n clearErrors();\n }}\n invalid={isInvalid}\n name=\"server.analytics_property_id\"\n type=\"number\"\n label={}\n />\n {\n clearErrors();\n }}\n invalid={isInvalid}\n name=\"client.analytics.tracking_code\"\n placeholder=\"G-******\"\n min=\"1\"\n max=\"20\"\n description={\n \n }\n label={}\n />\n }\n description={\n \n }\n />\n \n )}\n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {User} from '@common/auth/user';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {toast} from '@common/ui/toast/toast';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {message} from '@common/i18n/message';\nimport {useNavigate} from '@common/utils/hooks/use-navigate';\n\ninterface Response extends BackendResponse {\n user: User;\n}\n\nexport interface UpdateUserPayload\n extends Omit, 'email_verified_at'> {\n email_verified_at?: boolean;\n id: number;\n}\n\nexport function useUpdateUser(form: UseFormReturn) {\n const navigate = useNavigate();\n return useMutation({\n mutationFn: (props: UpdateUserPayload) => updateUser(props),\n onSuccess: (response, props) => {\n toast(message('User updated'));\n queryClient.invalidateQueries({queryKey: ['users']});\n navigate('/admin/users');\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction updateUser({id, ...other}: UpdateUserPayload): Promise {\n if (other.roles) {\n other.roles = other.roles.map(r => r.id) as any;\n }\n return apiClient.put(`users/${id}`, other).then(r => r.data);\n}\n","import {FieldValues, SubmitHandler, UseFormReturn} from 'react-hook-form';\nimport clsx from 'clsx';\nimport {ReactNode} from 'react';\nimport {Link} from 'react-router-dom';\nimport {useValueLists} from '../../http/value-lists';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {FormSwitch} from '../../ui/forms/toggle/switch';\nimport {FormFileSizeField} from '../../ui/forms/input-field/file-size-field';\nimport {LinkStyle} from '../../ui/buttons/external-link';\nimport {FormPermissionSelector} from '../../auth/ui/permission-selector';\nimport {Trans} from '../../i18n/trans';\nimport {FormChipField} from '../../ui/forms/input-field/chip-field/form-chip-field';\nimport {Item} from '../../ui/forms/listbox/item';\nimport {CrupdateResourceLayout} from '../crupdate-resource-layout';\nimport {useSettings} from '../../core/settings/use-settings';\n\ninterface Props {\n onSubmit: SubmitHandler;\n form: UseFormReturn;\n title: ReactNode;\n subTitle?: ReactNode;\n isLoading: boolean;\n avatarManager: ReactNode;\n resendEmailButton?: ReactNode;\n children?: ReactNode;\n}\nexport function CrupdateUserForm({\n onSubmit,\n form,\n title,\n subTitle,\n isLoading,\n avatarManager,\n resendEmailButton,\n children,\n}: Props) {\n const {require_email_confirmation} = useSettings();\n const {data: valueLists} = useValueLists(['roles', 'permissions']);\n\n return (\n \n
\n {avatarManager}\n
\n {children}\n }\n />\n }\n />\n
\n
\n\n
\n \n }\n >\n \n \n {resendEmailButton}\n
\n }\n description={\n (\n \n {parts}\n \n ),\n }}\n message=\"Total storage space all user uploads are allowed to take up. If left empty, this value will be inherited from any roles or subscriptions user has, or from 'Available space' setting in Uploading settings page.\"\n />\n }\n />\n }\n suggestions={valueLists?.roles}\n >\n {chip => (\n \n {chip.name}\n \n )}\n \n
\n
\n \n
\n \n
\n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const ReportIcon = createSvgIcon(\n [,,,]\n, 'ReportOutlined');\n","import {useForm} from 'react-hook-form';\nimport {useParams} from 'react-router-dom';\nimport React, {useEffect} from 'react';\nimport {useUser} from '../../auth/ui/use-user';\nimport {UpdateUserPayload, useUpdateUser} from './requests/update-user';\nimport {Button} from '../../ui/buttons/button';\nimport {useResendVerificationEmail} from '../../auth/requests/use-resend-verification-email';\nimport {useUploadAvatar} from '../../auth/ui/account-settings/avatar/upload-avatar';\nimport {useRemoveAvatar} from '../../auth/ui/account-settings/avatar/remove-avatar';\nimport {CrupdateUserForm} from './crupdate-user-form';\nimport {User} from '../../auth/user';\nimport {Trans} from '../../i18n/trans';\nimport {FullPageLoader} from '../../ui/progress/full-page-loader';\nimport {useSettings} from '../../core/settings/use-settings';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';\nimport {FormImageSelector} from '@common/ui/images/image-selector';\nimport {queryClient} from '@common/http/query-client';\nimport {ReportIcon} from '@common/icons/material/Report';\n\nexport function UpdateUserPage() {\n const form = useForm();\n const {require_email_confirmation} = useSettings();\n const {userId} = useParams();\n const updateUser = useUpdateUser(form);\n const resendConfirmationEmail = useResendVerificationEmail();\n const {data, isLoading} = useUser(userId!, {\n with: ['subscriptions', 'roles', 'permissions', 'bans'],\n });\n const banReason = data?.user.bans?.[0]?.comment;\n\n useEffect(() => {\n if (data?.user && !form.getValues().id) {\n form.reset({\n first_name: data.user.first_name,\n last_name: data.user.last_name,\n roles: data.user.roles,\n permissions: data.user.permissions,\n id: data.user.id,\n email_verified_at: Boolean(data.user.email_verified_at),\n available_space: data.user.available_space,\n avatar: data.user.avatar,\n });\n }\n }, [data?.user, form]);\n\n if (isLoading) {\n return ;\n }\n\n const resendEmailButton = (\n {\n resendConfirmationEmail.mutate({email: data!.user.email});\n }}\n >\n \n \n );\n\n return (\n {\n updateUser.mutate(newValues);\n }}\n form={form}\n title={\n \n }\n subTitle={\n banReason && (\n
\n \n
\n \n
\n
\n )\n }\n isLoading={updateUser.isPending}\n avatarManager={\n {\n queryClient.invalidateQueries({queryKey: ['users']});\n }}\n />\n }\n resendEmailButton={resendEmailButton}\n >\n }\n />\n \n );\n}\n\ninterface AvatarSectionProps {\n user: User;\n onChange: () => void;\n}\nfunction AvatarSection({user, onChange}: AvatarSectionProps) {\n const uploadAvatar = useUploadAvatar({user});\n const removeAvatar = useRemoveAvatar({user});\n return (\n \n }\n previewSize=\"w-90 h-90\"\n showRemoveButton\n onChange={url => {\n if (url) {\n uploadAvatar.mutate({url});\n } else {\n removeAvatar.mutate();\n }\n onChange();\n }}\n />\n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {User} from '../../../auth/user';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {apiClient, queryClient} from '../../../http/query-client';\nimport {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';\nimport {onFormQueryError} from '../../../errors/on-form-query-error';\nimport {message} from '../../../i18n/message';\nimport {useNavigate} from '../../../utils/hooks/use-navigate';\n\ninterface Response extends BackendResponse {\n user: User;\n}\n\nexport interface CreateUserPayload\n extends Omit, 'email_verified_at'> {\n email_verified_at?: boolean;\n}\n\nexport function useCreateUser(form: UseFormReturn) {\n const navigate = useNavigate();\n return useMutation({\n mutationFn: (props: CreateUserPayload) => createUser(props),\n onSuccess: () => {\n toast(message('User created'));\n queryClient.invalidateQueries({queryKey: DatatableDataQueryKey('users')});\n navigate('/admin/users');\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction createUser(payload: CreateUserPayload): Promise {\n if (payload.roles) {\n payload.roles = payload.roles.map(r => r.id) as any;\n }\n return apiClient.post('users', payload).then(r => r.data);\n}\n","import {useForm} from 'react-hook-form';\nimport React from 'react';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {CreateUserPayload, useCreateUser} from './requests/create-user';\nimport {CrupdateUserForm} from './crupdate-user-form';\nimport {FileUploadProvider} from '../../uploads/uploader/file-upload-provider';\nimport {Trans} from '../../i18n/trans';\nimport {FormImageSelector} from '@common/ui/images/image-selector';\n\nexport function CreateUserPage() {\n const form = useForm();\n const createUser = useCreateUser(form);\n\n const avatarManager = (\n \n }\n previewSize=\"w-90 h-90\"\n showRemoveButton\n />\n \n );\n\n return (\n {\n createUser.mutate(newValues);\n }}\n form={form}\n title={}\n isLoading={createUser.isPending}\n avatarManager={avatarManager}\n >\n }\n />\n }\n />\n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const TranslateIcon = createSvgIcon(\n \n, 'TranslateOutlined');\n","import {useQuery} from '@tanstack/react-query';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {Localization} from '../../i18n/localization';\nimport {apiClient} from '../../http/query-client';\n\nexport interface FetchLocaleWithLinesResponse extends BackendResponse {\n localization: Localization;\n}\n\nexport const getLocalWithLinesQueryKey = (localeId?: number | string) => {\n const key: (string | number)[] = ['getLocaleWithLines'];\n if (localeId != null) {\n key.push(localeId);\n }\n return key;\n};\n\nexport function useLocaleWithLines(localeId: number | string) {\n return useQuery({\n queryKey: getLocalWithLinesQueryKey(localeId),\n queryFn: () => fetchLocaleWithLines(localeId),\n staleTime: Infinity,\n });\n}\n\nfunction fetchLocaleWithLines(\n localeId: number | string,\n): Promise {\n return apiClient\n .get(`localizations/${localeId}`)\n .then(response => response.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {toast} from '../../ui/toast/toast';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {apiClient, queryClient} from '../../http/query-client';\nimport {message} from '../../i18n/message';\nimport {DatatableDataQueryKey} from '../../datatable/requests/paginated-resources';\nimport {Localization} from '../../i18n/localization';\nimport {onFormQueryError} from '../../errors/on-form-query-error';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\nimport {getLocalWithLinesQueryKey} from './use-locale-with-lines';\n\ninterface Response extends BackendResponse {\n localization: Localization;\n}\n\nfunction UpdateLocalization({\n id,\n ...other\n}: Partial): Promise {\n return apiClient.put(`localizations/${id}`, other).then(r => r.data);\n}\n\nexport function useUpdateLocalization(\n form?: UseFormReturn>,\n) {\n return useMutation({\n mutationFn: (props: Partial) => UpdateLocalization(props),\n onSuccess: () => {\n toast(message('Localization updated'));\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('localizations'),\n });\n queryClient.invalidateQueries({queryKey: getLocalWithLinesQueryKey()});\n },\n onError: r => (form ? onFormQueryError(r, form) : showHttpErrorToast(r)),\n });\n}\n","import {useForm} from 'react-hook-form';\nimport {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {Trans} from '../../i18n/trans';\nimport {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\nimport {Form} from '../../ui/forms/form';\nimport {Localization} from '../../i18n/localization';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {useValueLists} from '../../http/value-lists';\nimport {FormSelect, Option} from '../../ui/forms/select/select';\nimport {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';\nimport {Button} from '../../ui/buttons/button';\nimport {useUpdateLocalization} from './update-localization';\nimport {message} from '@common/i18n/message';\nimport {useTrans} from '@common/i18n/use-trans';\n\ninterface UpdateLocalizationDialogProps {\n localization: Localization;\n}\nexport function UpdateLocalizationDialog({\n localization,\n}: UpdateLocalizationDialogProps) {\n const {trans} = useTrans();\n const {formId, close} = useDialogContext();\n const form = useForm>({\n defaultValues: {\n id: localization.id,\n name: localization.name,\n language: localization.language,\n },\n });\n\n const {data} = useValueLists(['languages']);\n const languages = data?.languages || [];\n\n const updateLocalization = useUpdateLocalization(form);\n\n return (\n \n \n \n \n \n {\n updateLocalization.mutate(values, {onSuccess: close});\n }}\n >\n }\n className=\"mb-30\"\n required\n />\n }\n selectionMode=\"single\"\n showSearchField\n searchPlaceholder={trans(message('Search languages'))}\n >\n {languages.map(language => (\n \n ))}\n \n \n \n \n \n \n \n \n \n \n );\n}\n","import {useMutation, useQueryClient} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {toast} from '../../ui/toast/toast';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {apiClient} from '../../http/query-client';\nimport {message} from '../../i18n/message';\nimport {DatatableDataQueryKey} from '../../datatable/requests/paginated-resources';\nimport {onFormQueryError} from '../../errors/on-form-query-error';\nimport {Localization} from '../../i18n/localization';\n\ninterface Response extends BackendResponse {\n localization: Localization;\n}\n\nexport interface CreateLocalizationPayload {\n name: string;\n language: string;\n}\n\nfunction createLocalization(\n payload: CreateLocalizationPayload,\n): Promise {\n return apiClient.post(`localizations`, payload).then(r => r.data);\n}\n\nexport function useCreateLocalization(\n form: UseFormReturn,\n) {\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: (props: CreateLocalizationPayload) => createLocalization(props),\n onSuccess: () => {\n toast(message('Localization created'));\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('localizations'),\n });\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n","import {useForm} from 'react-hook-form';\nimport {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {Trans} from '../../i18n/trans';\nimport {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\nimport {Form} from '../../ui/forms/form';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {useValueLists} from '../../http/value-lists';\nimport {FormSelect, Option} from '../../ui/forms/select/select';\nimport {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';\nimport {Button} from '../../ui/buttons/button';\nimport {\n CreateLocalizationPayload,\n useCreateLocalization,\n} from './create-localization';\nimport {message} from '@common/i18n/message';\nimport {useTrans} from '@common/i18n/use-trans';\n\nexport function CreateLocationDialog() {\n const {trans} = useTrans();\n const {formId, close} = useDialogContext();\n const form = useForm({\n defaultValues: {\n language: 'en',\n },\n });\n\n const {data} = useValueLists(['languages']);\n const languages = data?.languages || [];\n\n const createLocalization = useCreateLocalization(form);\n\n return (\n \n \n \n \n \n {\n createLocalization.mutate(values, {onSuccess: close});\n }}\n >\n }\n className=\"mb-30\"\n required\n />\n }\n selectionMode=\"single\"\n showSearchField\n searchPlaceholder={trans(message('Search languages'))}\n >\n {languages.map(language => (\n \n ))}\n \n \n \n \n \n \n \n \n \n \n );\n}\n","export default \"__VITE_ASSET__5bb85b7d__\"","import {useMutation} from '@tanstack/react-query';\nimport {toast} from '../../ui/toast/toast';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {apiClient, queryClient} from '../../http/query-client';\nimport {message} from '../../i18n/message';\nimport {DatatableDataQueryKey} from '../../datatable/requests/paginated-resources';\nimport {Localization} from '../../i18n/localization';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\nimport {getLocalWithLinesQueryKey} from './use-locale-with-lines';\nimport {UploadedFile} from '@common/uploads/uploaded-file';\n\ninterface Response extends BackendResponse {\n localization: Localization;\n}\n\ninterface Payload {\n file: UploadedFile;\n localeId: string | number;\n}\n\nexport function useUploadTranslationFile() {\n return useMutation({\n mutationFn: (payload: Payload) => uploadFile(payload),\n onSuccess: async () => {\n await queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('localizations'),\n });\n await queryClient.invalidateQueries({\n queryKey: getLocalWithLinesQueryKey(),\n });\n toast(message('Translation file uploaded'));\n },\n onError: r => showHttpErrorToast(r),\n });\n}\n\nfunction uploadFile({localeId, file}: Payload): Promise {\n const data = new FormData();\n data.append('file', file.native);\n return apiClient\n .post(`localizations/${localeId}/upload`, data)\n .then(r => r.data);\n}\n","import React, {Fragment} from 'react';\nimport {Link} from 'react-router-dom';\nimport {DataTablePage} from '../../datatable/page/data-table-page';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {FormattedDate} from '../../i18n/formatted-date';\nimport {ColumnConfig} from '../../datatable/column-config';\nimport {Trans} from '../../i18n/trans';\nimport {Localization} from '../../i18n/localization';\nimport {TranslateIcon} from '../../icons/material/Translate';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {UpdateLocalizationDialog} from './update-localization-dialog';\nimport {Tooltip} from '../../ui/tooltip/tooltip';\nimport {CreateLocationDialog} from './create-localization-dialog';\nimport {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';\nimport aroundTheWorldSvg from './around-the-world.svg';\nimport {DataTableAddItemButton} from '../../datatable/data-table-add-item-button';\nimport {DeleteSelectedItemsAction} from '../../datatable/page/delete-selected-items-action';\nimport {\n Menu,\n MenuItem,\n MenuTrigger,\n} from '@common/ui/navigation/menu/menu-trigger';\nimport {openDialog} from '@common/ui/overlays/store/dialog-store';\nimport {downloadFileFromUrl} from '@common/uploads/utils/download-file-from-url';\nimport {MoreVertIcon} from '@common/icons/material/MoreVert';\nimport {UploadInputType} from '@common/uploads/types/upload-input-config';\nimport {FileUploadProvider} from '@common/uploads/uploader/file-upload-provider';\nimport {useUploadTranslationFile} from '@common/admin/translations/use-upload-translation-file';\nimport {openUploadWindow} from '@common/uploads/utils/open-upload-window';\n\nconst columnConfig: ColumnConfig[] = [\n {\n key: 'name',\n allowsSorting: true,\n sortingKey: 'name',\n visibleInMode: 'all',\n width: 'flex-3 min-w-200',\n header: () => ,\n body: locale => locale.name,\n },\n {\n key: 'language',\n allowsSorting: true,\n sortingKey: 'language',\n header: () => ,\n body: locale => locale.language,\n },\n {\n key: 'updatedAt',\n allowsSorting: true,\n width: 'w-100',\n header: () => ,\n body: locale => ,\n },\n {\n key: 'actions',\n header: () => ,\n hideHeader: true,\n align: 'end',\n width: 'w-84 flex-shrink-0',\n visibleInMode: 'all',\n body: locale => {\n return (\n
\n }>\n \n \n \n \n\n \n \n \n
\n );\n },\n },\n];\n\nexport function LocalizationIndex() {\n return (\n }\n columns={columnConfig}\n actions={}\n selectedActions={}\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n );\n}\n\nfunction Actions() {\n return (\n \n \n \n \n \n \n \n \n );\n}\n\ninterface RowActionsMenuTriggerProps {\n locale: Localization;\n}\nfunction RowActionsMenuTrigger({locale}: RowActionsMenuTriggerProps) {\n const uploadFile = useUploadTranslationFile();\n return (\n \n \n \n \n \n \n \n \n \n openDialog(UpdateLocalizationDialog, {localization: locale})\n }\n >\n \n \n \n downloadFileFromUrl(`api/v1/localizations/${locale.id}/download`)\n }\n >\n \n \n {\n const files = await openUploadWindow({\n types: [UploadInputType.json],\n });\n if (files.length == 1) {\n uploadFile.mutate({localeId: locale.id, file: files[0]});\n }\n }}\n >\n \n \n \n \n );\n}\n","import {useForm} from 'react-hook-form';\nimport {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {Trans} from '../../i18n/trans';\nimport {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\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 {SectionHelper} from '../../ui/section-helper';\n\ninterface FormValue {\n key: string;\n value: string;\n}\n\nexport function NewTranslationDialog() {\n const {formId, close} = useDialogContext();\n const form = useForm();\n\n return (\n \n \n \n \n \n {\n close(values);\n }}\n >\n \n }\n description={\n \n }\n />\n }\n className=\"mb-30\"\n required\n />\n }\n required\n />\n \n \n \n \n \n \n \n );\n}\n","import React, {useMemo, useRef, useState} from 'react';\nimport {useParams} from 'react-router-dom';\nimport {useLocaleWithLines} from './use-locale-with-lines';\nimport {Trans} from '../../i18n/trans';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {Button} from '../../ui/buttons/button';\nimport {Breadcrumb} from '../../ui/breadcrumbs/breadcrumb';\nimport {BreadcrumbItem} from '../../ui/breadcrumbs/breadcrumb-item';\nimport {TextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {useTrans} from '../../i18n/use-trans';\nimport {SearchIcon} from '../../icons/material/Search';\nimport {CloseIcon} from '../../icons/material/Close';\nimport {AddIcon} from '../../icons/material/Add';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {NewTranslationDialog} from './new-translation-dialog';\nimport {useUpdateLocalization} from './update-localization';\nimport {Localization} from '../../i18n/localization';\nimport {FullPageLoader} from '../../ui/progress/full-page-loader';\nimport {useIsMobileMediaQuery} from '../../utils/hooks/is-mobile-media-query';\nimport {useVirtualizer} from '@tanstack/react-virtual';\nimport {useNavigate} from '../../utils/hooks/use-navigate';\nimport {useUploadTranslationFile} from '@common/admin/translations/use-upload-translation-file';\nimport {\n Menu,\n MenuItem,\n MenuTrigger,\n} from '@common/ui/navigation/menu/menu-trigger';\nimport {MoreVertIcon} from '@common/icons/material/MoreVert';\nimport {downloadFileFromUrl} from '@common/uploads/utils/download-file-from-url';\nimport {openUploadWindow} from '@common/uploads/utils/open-upload-window';\nimport {UploadInputType} from '@common/uploads/types/upload-input-config';\n\ntype Lines = Record;\n\nexport function TranslationManagementPage() {\n const {localeId} = useParams();\n\n const {data, isLoading} = useLocaleWithLines(localeId!);\n const localization = data?.localization;\n\n if (isLoading || !localization) {\n return ;\n }\n\n return
;\n}\n\ninterface FormProps {\n localization: Localization;\n}\nfunction Form({localization}: FormProps) {\n const [lines, setLines] = useState(localization.lines || {});\n\n const navigate = useNavigate();\n const updateLocalization = useUpdateLocalization();\n const [searchQuery, setSearchQuery] = useState('');\n\n return (\n {\n e.preventDefault();\n updateLocalization.mutate(\n {id: localization.id, lines},\n {\n onSuccess: () => {\n navigate('/admin/localizations');\n },\n },\n );\n }}\n >\n \n \n \n );\n}\n\ninterface HeaderProps {\n localization: Localization;\n lines: Lines;\n setLines: (lines: Lines) => void;\n searchQuery: string;\n setSearchQuery: (value: string) => void;\n isLoading: boolean;\n}\nfunction Header({\n localization,\n searchQuery,\n setSearchQuery,\n isLoading,\n lines,\n setLines,\n}: HeaderProps) {\n const navigate = useNavigate();\n const isMobile = useIsMobileMediaQuery();\n const {trans} = useTrans();\n\n return (\n
\n \n {\n navigate('/admin/localizations');\n }}\n >\n \n \n \n \n \n \n
\n
\n setSearchQuery(e.target.value)}\n startAdornment={}\n placeholder={trans({message: 'Type to search...'})}\n />\n
\n {\n if (newTranslation) {\n const newLines = {...lines};\n newLines[newTranslation.key] = newTranslation.value;\n setLines(newLines);\n }\n }}\n >\n {!isMobile && (\n }\n >\n \n \n )}\n \n \n \n \n {isMobile ? (\n \n ) : (\n \n )}\n \n
\n
\n );\n}\n\ninterface LinesListProps {\n searchQuery?: string;\n lines: Lines;\n setLines: (lines: Lines) => void;\n}\nfunction LinesList({searchQuery, lines, setLines}: LinesListProps) {\n const filteredLines = useMemo(() => {\n return Object.entries(lines).filter(([id, translation]) => {\n const lowerCaseQuery = searchQuery?.toLowerCase();\n return (\n !lowerCaseQuery ||\n id?.toLowerCase().includes(lowerCaseQuery) ||\n translation?.toLowerCase().includes(lowerCaseQuery)\n );\n });\n }, [lines, searchQuery]);\n\n const ref = useRef(null);\n const rowVirtualizer = useVirtualizer({\n count: filteredLines.length,\n getScrollElement: () => ref.current,\n estimateSize: () => 123,\n });\n\n return (\n
\n \n {rowVirtualizer.getVirtualItems().map(virtualItem => {\n const [id, translation] = filteredLines[virtualItem.index];\n return (\n \n
\n
\n \n {id}\n \n {\n const newLines = {...lines};\n delete newLines[id];\n setLines(newLines);\n }}\n >\n \n \n
\n
\n {\n const newLines = {...lines};\n newLines[id] = e.target.value;\n setLines(newLines);\n }}\n />\n
\n
\n
\n );\n })}\n \n \n );\n}\n\ninterface ActionsMenuTriggerProps {\n locale: Localization;\n}\nfunction ActionsMenuTrigger({locale}: ActionsMenuTriggerProps) {\n const uploadFile = useUploadTranslationFile();\n return (\n \n \n \n \n \n \n downloadFileFromUrl(`api/v1/localizations/${locale.id}/download`)\n }\n >\n \n \n {\n const files = await openUploadWindow({\n types: [UploadInputType.json],\n });\n if (files.length == 1) {\n uploadFile.mutate({localeId: locale.id, file: files[0]});\n }\n }}\n >\n \n \n \n \n );\n}\n","import {useContext} from 'react';\nimport {\n AdConfig,\n SiteConfigContext,\n} from '../../core/settings/site-config-context';\nimport {Form} from '../../ui/forms/form';\nimport {useForm} from 'react-hook-form';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../i18n/trans';\nimport {Button} from '../../ui/buttons/button';\nimport {FormSwitch} from '../../ui/forms/toggle/switch';\nimport {useAdminSettings} from '../settings/requests/use-admin-settings';\nimport {ProgressCircle} from '../../ui/progress/progress-circle';\nimport {Settings} from '../../core/settings/settings';\nimport {\n AdminSettingsWithFiles,\n useUpdateAdminSettings,\n} from '../settings/requests/update-admin-settings';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {ImageZoomDialog} from '../../ui/overlays/dialog/image-zoom-dialog';\nimport {StaticPageTitle} from '../../seo/static-page-title';\n\nexport function AdsPage() {\n const query = useAdminSettings();\n\n return (\n
\n \n \n \n

\n \n

\n {query.isLoading ? (\n \n ) : (\n \n )}\n
\n );\n}\n\ninterface AdsFormProps {\n defaultValues: Settings['ads'];\n}\nfunction AdsForm({defaultValues}: AdsFormProps) {\n const {\n admin: {ads},\n } = useContext(SiteConfigContext);\n\n const form = useForm({\n defaultValues: {client: {ads: defaultValues}},\n });\n const updateSettings = useUpdateAdminSettings(form);\n\n return (\n {\n updateSettings.mutate(value);\n }}\n >\n {ads.map(ad => {\n return ;\n })}\n \n }\n >\n \n \n \n \n \n \n );\n}\n\ninterface AdSectionProps {\n adConfig: AdConfig;\n}\nfunction AdSection({adConfig}: AdSectionProps) {\n return (\n
\n }\n />\n \n \n \n \n \n \n
\n );\n}\n","import {NavLink} from 'react-router-dom';\nimport {AppearanceButton} from './appearance-button';\nimport {useAppearanceStore} from './appearance-store';\nimport {Trans} from '../../i18n/trans';\nimport {Fragment, useMemo} from 'react';\n\nexport function SectionList() {\n const sections = useAppearanceStore(s => s.config?.sections);\n const sortedSection = useMemo(() => {\n if (!sections) return [];\n return Object.entries(sections || [])\n .map(([key, value]) => {\n return {\n ...value,\n key,\n };\n })\n .sort((a, b) => (a?.position || 1) - (b?.position || 1));\n }, [sections]);\n\n return (\n \n {sortedSection.map(section => {\n return (\n \n \n \n );\n })}\n \n );\n}\n","import {\n BackendFilter,\n FilterControlType,\n FilterOperator,\n} from '../../datatable/filters/backend-filter';\nimport {message} from '../../i18n/message';\nimport {\n createdAtFilter,\n updatedAtFilter,\n} from '@common/datatable/filters/timestamp-filters';\n\nexport const RoleIndexPageFilters: BackendFilter[] = [\n {\n key: 'type',\n label: message('Type'),\n description: message('Type of the role'),\n defaultOperator: FilterOperator.ne,\n control: {\n type: FilterControlType.Select,\n defaultValue: '01',\n options: [\n {\n key: '01',\n label: message('Sitewide'),\n value: 'sitewide',\n },\n {\n key: '02',\n label: message('Workspace'),\n value: 'workspace',\n },\n ],\n },\n },\n createdAtFilter({\n description: message('Date role was created'),\n }),\n updatedAtFilter({\n description: message('Date role was last updated'),\n }),\n];\n","import React, {Fragment} from 'react';\nimport {Link} from 'react-router-dom';\nimport {DataTablePage} from '../../datatable/page/data-table-page';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {EditIcon} from '../../icons/material/Edit';\nimport {FormattedDate} from '../../i18n/formatted-date';\nimport {ColumnConfig} from '../../datatable/column-config';\nimport {Trans} from '../../i18n/trans';\nimport {Role} from '../../auth/role';\nimport teamSvg from './team.svg';\nimport {DeleteSelectedItemsAction} from '../../datatable/page/delete-selected-items-action';\nimport {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';\nimport {RoleIndexPageFilters} from './role-index-page-filters';\nimport {DataTableExportCsvButton} from '../../datatable/csv-export/data-table-export-csv-button';\nimport {DataTableAddItemButton} from '../../datatable/data-table-add-item-button';\n\nconst columnConfig: ColumnConfig[] = [\n {\n key: 'name',\n allowsSorting: true,\n visibleInMode: 'all',\n header: () => ,\n body: role => (\n
\n
\n \n
\n
\n {role.description ? : undefined}\n
\n
\n ),\n },\n {\n key: 'type',\n maxWidth: 'max-w-100',\n allowsSorting: true,\n header: () => ,\n body: role => ,\n },\n {\n key: 'updated_at',\n maxWidth: 'max-w-100',\n allowsSorting: true,\n header: () => ,\n body: role => ,\n },\n {\n key: 'actions',\n header: () => ,\n hideHeader: true,\n visibleInMode: 'all',\n align: 'end',\n width: 'w-42 flex-shrink-0',\n body: role => {\n return (\n \n \n \n \n \n );\n },\n },\n];\n\nexport function RolesIndexPage() {\n return (\n }\n columns={columnConfig}\n filters={RoleIndexPageFilters}\n actions={}\n selectedActions={}\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n );\n}\n\nfunction Actions() {\n return (\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';\nimport {Role} from '@common/auth/role';\nimport {useParams} from 'react-router-dom';\n\nconst Endpoint = (id: number | string) => `roles/${id}`;\n\nexport interface FetchRoleResponse extends BackendResponse {\n role: Role;\n}\n\nexport function useRole() {\n const {roleId} = useParams();\n return useQuery({\n queryKey: [Endpoint(roleId!)],\n queryFn: () => fetchRole(roleId!),\n });\n}\n\nfunction fetchRole(roleId: number | string): Promise {\n return apiClient.get(Endpoint(roleId)).then(response => response.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient, queryClient} from '../../../http/query-client';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {Role} from '../../../auth/role';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {message} from '../../../i18n/message';\nimport {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';\nimport {showHttpErrorToast} from '../../../utils/http/show-http-error-toast';\nimport {useNavigate} from '../../../utils/hooks/use-navigate';\n\ninterface Response extends BackendResponse {\n role: Role;\n}\n\ninterface Payload extends Partial {\n id: number;\n}\n\nconst Endpoint = (id: number) => `roles/${id}`;\n\nexport function useUpdateRole() {\n const {trans} = useTrans();\n const navigate = useNavigate();\n return useMutation({\n mutationFn: (payload: Payload) => updateRole(payload),\n onSuccess: response => {\n toast(trans(message('Role updated')));\n queryClient.invalidateQueries({queryKey: [Endpoint(response.role.id)]});\n queryClient.invalidateQueries({queryKey: DatatableDataQueryKey('roles')});\n navigate('/admin/roles');\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction updateRole({id, ...payload}: Payload): Promise {\n return apiClient.put(Endpoint(id), payload).then(r => r.data);\n}\n","import {Role} from '../../../auth/role';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {useFormContext} from 'react-hook-form';\nimport {FormTextField} from '../../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../../i18n/trans';\nimport {message} from '../../../i18n/message';\nimport {FormSelect} from '../../../ui/forms/select/select';\nimport {Item} from '../../../ui/forms/listbox/item';\nimport {FormSwitch} from '../../../ui/forms/toggle/switch';\nimport {FormPermissionSelector} from '../../../auth/ui/permission-selector';\nimport {useSettings} from '../../../core/settings/use-settings';\nimport {Button} from '@common/ui/buttons/button';\n\ninterface CrupdateRolePageSettingsPanelProps {\n isInternal?: boolean;\n}\nexport function CrupdateRolePageSettingsPanel({\n isInternal = false,\n}: CrupdateRolePageSettingsPanelProps) {\n const {trans} = useTrans();\n const {workspaces} = useSettings();\n const {watch, setValue} = useFormContext();\n const watchedType = watch('type');\n\n return (\n <>\n }\n name=\"name\"\n className=\"mb-20\"\n required\n />\n }\n name=\"description\"\n inputElementType=\"textarea\"\n placeholder={trans(message('Role description...'))}\n rows={4}\n className=\"mb-20\"\n />\n {workspaces.integrated && (\n }\n name=\"type\"\n selectionMode=\"single\"\n className=\"mb-20\"\n description={\n \n }\n >\n \n \n \n \n \n \n \n )}\n {!isInternal && (\n <>\n \n }\n >\n \n \n {watchedType === 'sitewide' && (\n \n }\n >\n \n \n )}\n \n )}\n
\n

\n \n

\n setValue('permissions', [])}\n >\n \n \n
\n \n \n );\n}\n","import {Dialog} from '../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../ui/overlays/dialog/dialog-header';\nimport {Trans} from '../i18n/trans';\nimport {DialogBody} from '../ui/overlays/dialog/dialog-body';\nimport {TextField} from '../ui/forms/input-field/text-field/text-field';\nimport {SearchIcon} from '../icons/material/Search';\nimport {useState} from 'react';\nimport {useTrans} from '../i18n/use-trans';\nimport {message} from '../i18n/message';\nimport {Avatar} from '../ui/images/avatar';\nimport {NormalizedModel} from '../datatable/filters/normalized-model';\nimport {IllustratedMessage} from '../ui/images/illustrated-message';\nimport {SvgImage} from '../ui/images/svg-image/svg-image';\nimport teamSvg from '../admin/roles/team.svg';\nimport {useDialogContext} from '../ui/overlays/dialog/dialog-context';\nimport {useNormalizedModels} from './queries/use-normalized-models';\n\ninterface SelectUserDialogProps {\n onUserSelected: (user: NormalizedModel) => void;\n}\n\nexport function SelectUserDialog({onUserSelected}: SelectUserDialogProps) {\n const {close} = useDialogContext();\n const [searchTerm, setSearchTerm] = useState('');\n const {trans} = useTrans();\n const query = useNormalizedModels('normalized-models/user', {\n query: searchTerm,\n perPage: 14,\n });\n const users = query.data?.results || [];\n\n const emptyStateMessage = (\n }\n description={}\n image={}\n />\n );\n\n const selectUser = (user: NormalizedModel) => {\n close();\n onUserSelected(user);\n };\n\n return (\n \n \n \n \n \n }\n placeholder={trans(message('Search for user by name or email'))}\n value={searchTerm}\n onChange={e => {\n setSearchTerm(e.target.value);\n }}\n />\n {!query.isLoading && !users.length && emptyStateMessage}\n
\n {users.map(user => (\n \n ))}\n
\n
\n
\n );\n}\n\ninterface UserListItemProps {\n user: NormalizedModel;\n onUserSelected: (user: NormalizedModel) => void;\n}\nfunction UserListItem({user, onUserSelected}: UserListItemProps) {\n return (\n {\n onUserSelected(user);\n }}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onUserSelected(user);\n }\n }}\n >\n \n
\n
{user.name}
\n
\n {user.description}\n
\n
\n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient} from '../../../http/query-client';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {message} from '../../../i18n/message';\nimport {Role} from '../../../auth/role';\nimport {showHttpErrorToast} from '../../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\ninterface Payload {\n userIds: number[];\n}\n\nexport function useRemoveUsersFromRole(role: Role) {\n return useMutation({\n mutationFn: ({userIds}: Payload) =>\n removeUsersFromRole({userIds, roleId: role.id}),\n onSuccess: (response, payload) => {\n toast(\n message('Removed [one 1 user|other :count users] from “{role}“', {\n values: {count: payload.userIds.length, role: role.name},\n }),\n );\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction removeUsersFromRole({\n roleId,\n userIds,\n}: Payload & {roleId: number}): Promise {\n return apiClient\n .post(`roles/${roleId}/remove-users`, {userIds})\n .then(r => r.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient} from '../../../http/query-client';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {message} from '../../../i18n/message';\nimport {Role} from '../../../auth/role';\nimport {showHttpErrorToast} from '../../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\ninterface Payload {\n userIds: number[];\n}\n\nexport function useAddUsersToRole(role: Role) {\n return useMutation({\n mutationFn: ({userIds}: Payload) =>\n addUsersToRole({userIds, roleId: role.id}),\n onSuccess: (response, payload) => {\n toast(\n message('Assigned [one 1 user|other :count users] to {role}', {\n values: {count: payload.userIds.length, role: role.name},\n }),\n );\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction addUsersToRole({\n roleId,\n userIds,\n}: Payload & {roleId: number}): Promise {\n return apiClient\n .post(`roles/${roleId}/add-users`, {userIds})\n .then(r => r.data);\n}\n","import {Role} from '../../../auth/role';\nimport {ColumnConfig} from '../../../datatable/column-config';\nimport {User} from '../../../auth/user';\nimport {Trans} from '../../../i18n/trans';\nimport {NameWithAvatar} from '../../../datatable/column-templates/name-with-avatar';\nimport {FormattedDate} from '../../../i18n/formatted-date';\nimport React from 'react';\nimport teamSvg from '../team.svg';\nimport {DialogTrigger} from '../../../ui/overlays/dialog/dialog-trigger';\nimport {Button} from '../../../ui/buttons/button';\nimport {SelectUserDialog} from '../../../users/select-user-dialog';\nimport {queryClient} from '../../../http/query-client';\nimport {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';\nimport {DataTableEmptyStateMessage} from '../../../datatable/page/data-table-emty-state-message';\nimport {useDataTable} from '../../../datatable/page/data-table-context';\nimport {ConfirmationDialog} from '../../../ui/overlays/dialog/confirmation-dialog';\nimport {useRemoveUsersFromRole} from '../requests/use-remove-users-from-role';\nimport {useAddUsersToRole} from '../requests/use-add-users-to-role';\nimport {DataTable} from '../../../datatable/data-table';\nimport {useIsMobileMediaQuery} from '../../../utils/hooks/is-mobile-media-query';\n\nconst userColumn: ColumnConfig = {\n key: 'name',\n allowsSorting: true,\n sortingKey: 'email',\n header: () => ,\n body: user => (\n \n ),\n width: 'col-w-3',\n};\n\nconst desktopColumns: ColumnConfig[] = [\n userColumn,\n {\n key: 'first_name',\n allowsSorting: true,\n header: () => ,\n body: user => user.first_name,\n },\n {\n key: 'last_name',\n allowsSorting: true,\n header: () => ,\n body: user => user.last_name,\n },\n {\n key: 'created_at',\n allowsSorting: true,\n header: () => ,\n body: user => ,\n },\n];\n\nconst mobileColumns: ColumnConfig[] = [userColumn];\n\ninterface CrupdateRolePageUsersPanelProps {\n role: Role;\n}\nexport function EditRolePageUsersPanel({\n role,\n}: CrupdateRolePageUsersPanelProps) {\n const isMobile = useIsMobileMediaQuery();\n\n if (role.guests || role.type === 'workspace') {\n return (\n
\n }\n />\n
\n );\n }\n\n return (\n }\n selectedActions={}\n emptyStateMessage={\n \n }\n filteringTitle={}\n />\n }\n />\n );\n}\n\ninterface AssignUserActionProps {\n role: Role;\n}\nfunction AssignUserAction({role}: AssignUserActionProps) {\n const addUsers = useAddUsersToRole(role);\n return (\n \n \n {\n addUsers.mutate(\n {userIds: [user.id as number]},\n {\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('users', {\n roleId: `${role.id}`,\n }),\n });\n },\n },\n );\n }}\n />\n \n );\n}\n\ntype RemoveUsersActionProps = {\n role: Role;\n};\nexport function RemoveUsersAction({role}: RemoveUsersActionProps) {\n const removeUsers = useRemoveUsersFromRole(role);\n const {selectedRows} = useDataTable();\n\n return (\n {\n if (isConfirmed) {\n removeUsers.mutate(\n {userIds: selectedRows as number[]},\n {\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('users', {\n roleId: `${role.id}`,\n }),\n });\n },\n },\n );\n }\n }}\n >\n \n \n }\n body={}\n confirm={}\n isDanger\n />\n \n );\n}\n","import {useRole} from '../requests/use-role';\nimport {FullPageLoader} from '../../../ui/progress/full-page-loader';\nimport {Role} from '../../../auth/role';\nimport {Trans} from '../../../i18n/trans';\nimport {useForm} from 'react-hook-form';\nimport {Tabs} from '../../../ui/tabs/tabs';\nimport {Tab} from '../../../ui/tabs/tab';\nimport {TabList} from '../../../ui/tabs/tab-list';\nimport {TabPanel, TabPanels} from '../../../ui/tabs/tab-panels';\nimport {useUpdateRole} from '../requests/use-update-role';\nimport {CrupdateResourceLayout} from '../../crupdate-resource-layout';\nimport {CrupdateRolePageSettingsPanel} from './crupdate-role-settings-panel';\nimport {EditRolePageUsersPanel} from './edit-role-page-users-panel';\n\nexport function EditRolePage() {\n const query = useRole();\n\n if (query.status !== 'success') {\n return ;\n }\n\n return ;\n}\n\ninterface PageContentProps {\n role: Role;\n}\nfunction PageContent({role}: PageContentProps) {\n const form = useForm({defaultValues: role});\n const updateRole = useUpdateRole();\n\n return (\n {\n updateRole.mutate(values);\n }}\n title={}\n isLoading={updateRole.isPending}\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 {apiClient, queryClient} from '../../../http/query-client';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {Role} from '../../../auth/role';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {message} from '../../../i18n/message';\nimport {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';\nimport {onFormQueryError} from '../../../errors/on-form-query-error';\nimport {UseFormReturn} from 'react-hook-form';\n\ninterface Response extends BackendResponse {\n role: Role;\n}\n\nexport interface CreateRolePayload extends Partial {}\n\nconst Endpoint = 'roles';\n\nexport function useCreateRole(form: UseFormReturn) {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: (payload: CreateRolePayload) => createRole(payload),\n onSuccess: () => {\n toast(trans(message('Created new role')));\n queryClient.invalidateQueries({queryKey: DatatableDataQueryKey('roles')});\n },\n onError: r => onFormQueryError(r, form),\n });\n}\n\nfunction createRole({id, ...payload}: CreateRolePayload): Promise {\n return apiClient.post(Endpoint, payload).then(r => r.data);\n}\n","import {useForm} from 'react-hook-form';\nimport {CrupdateResourceLayout} from '../../crupdate-resource-layout';\nimport {Trans} from '../../../i18n/trans';\nimport {CrupdateRolePageSettingsPanel} from './crupdate-role-settings-panel';\nimport {CreateRolePayload, useCreateRole} from '../requests/user-create-role';\nimport {useNavigate} from '../../../utils/hooks/use-navigate';\n\nexport function CreateRolePage() {\n const form = useForm({defaultValues: {type: 'sitewide'}});\n const createRole = useCreateRole(form);\n const navigate = useNavigate();\n\n return (\n {\n createRole.mutate(values, {\n onSuccess: response => {\n navigate(`/admin/roles/${response.role.id}/edit`);\n },\n });\n }}\n title={}\n isLoading={createRole.isPending}\n >\n \n \n );\n}\n","import {\n BackendFilter,\n FilterControlType,\n FilterOperator,\n} from '../../datatable/filters/backend-filter';\nimport {message} from '../../i18n/message';\nimport {TagType} from '../../core/settings/site-config-context';\nimport {\n createdAtFilter,\n updatedAtFilter,\n} from '@common/datatable/filters/timestamp-filters';\n\nexport const TagIndexPageFilters = (types: TagType[]): BackendFilter[] => {\n return [\n {\n key: 'type',\n label: message('Type'),\n description: message('Type of the tag'),\n defaultOperator: FilterOperator.ne,\n control: {\n type: FilterControlType.Select,\n defaultValue: types[0].name,\n options: types.map(type => ({\n key: type.name,\n label: message(type.name),\n value: type.name,\n })),\n },\n },\n createdAtFilter({\n description: message('Date tag was created'),\n }),\n updatedAtFilter({\n description: message('Date tag was last updated'),\n }),\n ];\n};\n","export default \"__VITE_ASSET__8de61ea9__\"","import {Tag} from '../../tags/tag';\nimport {UseFormReturn} from 'react-hook-form';\nimport {Form} from '../../ui/forms/form';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {FormSelect} from '../../ui/forms/select/select';\nimport {Trans} from '../../i18n/trans';\nimport {Item} from '../../ui/forms/listbox/item';\nimport {useContext} from 'react';\nimport {SiteConfigContext} from '../../core/settings/site-config-context';\n\ninterface CrupdateTagFormProps {\n onSubmit: (values: Partial) => void;\n formId: string;\n form: UseFormReturn>;\n}\nexport function CrupdateTagForm({\n form,\n onSubmit,\n formId,\n}: CrupdateTagFormProps) {\n const {\n tags: {types},\n } = useContext(SiteConfigContext);\n const watchedType = form.watch('type');\n const isSystem = !!types.find(t => t.name === watchedType && t.system);\n\n return (\n
\n }\n description={}\n className=\"mb-20\"\n required\n autoFocus\n />\n }\n description={}\n className=\"mb-20\"\n />\n }\n name=\"type\"\n selectionMode=\"single\"\n disabled={isSystem}\n >\n {types\n .filter(t => !t.system)\n .map(type => (\n \n \n \n ))}\n \n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\nimport {Tag} from '@common/tags/tag';\nimport {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {UseFormReturn} from 'react-hook-form';\nimport {slugifyString} from '@common/utils/string/slugify-string';\n\ninterface Response extends BackendResponse {\n tag: Tag;\n}\n\ninterface Payload extends Partial {}\n\nexport function useCreateNewTag(form: UseFormReturn) {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: (props: Payload) => createNewTag(props),\n onSuccess: () => {\n toast(trans(message('Tag created')));\n queryClient.invalidateQueries({queryKey: DatatableDataQueryKey('tags')});\n },\n onError: err => onFormQueryError(err, form),\n });\n}\n\nfunction createNewTag(payload: Payload): Promise {\n payload.name = slugifyString(payload.name!);\n return apiClient.post('tags', payload).then(r => r.data);\n}\n","import {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {Trans} from '../../i18n/trans';\nimport {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {CrupdateTagForm} from './crupdate-tag-form';\nimport {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';\nimport {Button} from '../../ui/buttons/button';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\nimport {useCreateNewTag} from './requests/use-create-new-tag';\nimport {useContext} from 'react';\nimport {SiteConfigContext} from '../../core/settings/site-config-context';\nimport {useForm} from 'react-hook-form';\nimport {Tag} from '../../tags/tag';\n\nexport function CreateTagDialog() {\n const {close, formId} = useDialogContext();\n const {\n tags: {types},\n } = useContext(SiteConfigContext);\n const form = useForm>({\n defaultValues: {\n type: types[0].name,\n },\n });\n const createNewTag = useCreateNewTag(form);\n\n return (\n \n \n \n \n \n {\n createNewTag.mutate(values, {\n onSuccess: () => {\n close();\n },\n });\n }}\n />\n \n \n {\n close();\n }}\n >\n \n \n \n \n \n \n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\nimport {Tag} from '@common/tags/tag';\nimport {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {UseFormReturn} from 'react-hook-form';\nimport {slugifyString} from '@common/utils/string/slugify-string';\n\ninterface Response extends BackendResponse {\n tag: Tag;\n}\n\nexport interface UpdateTagPayload extends Partial {\n id: number;\n}\n\nexport function useUpdateTag(form: UseFormReturn) {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: (props: UpdateTagPayload) => updateTag(props),\n onSuccess: () => {\n toast(trans(message('Tag updated')));\n queryClient.invalidateQueries({queryKey: DatatableDataQueryKey('tags')});\n },\n onError: err => onFormQueryError(err, form),\n });\n}\n\nfunction updateTag({id, ...payload}: UpdateTagPayload): Promise {\n if (payload.name) {\n payload.name = slugifyString(payload.name!);\n }\n return apiClient.put(`tags/${id}`, payload).then(r => r.data);\n}\n","import {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {Trans} from '../../i18n/trans';\nimport {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {CrupdateTagForm} from './crupdate-tag-form';\nimport {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';\nimport {Button} from '../../ui/buttons/button';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\nimport {useForm} from 'react-hook-form';\nimport {Tag} from '../../tags/tag';\nimport {UpdateTagPayload, useUpdateTag} from './requests/use-update-tag';\n\ninterface UpdateTagDialogProps {\n tag: Tag;\n}\nexport function UpdateTagDialog({tag}: UpdateTagDialogProps) {\n const {close, formId} = useDialogContext();\n const form = useForm({\n defaultValues: {\n id: tag.id,\n name: tag.name,\n display_name: tag.display_name,\n type: tag.type,\n },\n });\n const updateTag = useUpdateTag(form);\n\n return (\n \n \n \n \n \n {\n updateTag.mutate(values as UpdateTagPayload, {\n onSuccess: () => {\n close();\n },\n });\n }}\n />\n \n \n {\n close();\n }}\n >\n \n \n \n \n \n \n \n );\n}\n","import React, {useContext, useMemo} from 'react';\nimport {DataTablePage} from '../../datatable/page/data-table-page';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {EditIcon} from '../../icons/material/Edit';\nimport {FormattedDate} from '../../i18n/formatted-date';\nimport {ColumnConfig} from '../../datatable/column-config';\nimport {Trans} from '../../i18n/trans';\nimport {DeleteSelectedItemsAction} from '../../datatable/page/delete-selected-items-action';\nimport {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';\nimport {Tag} from '../../tags/tag';\nimport {SiteConfigContext} from '../../core/settings/site-config-context';\nimport {TagIndexPageFilters} from './tag-index-page-filters';\nimport softwareEngineerSvg from './software-engineer.svg';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {CreateTagDialog} from './create-tag-dialog';\nimport {UpdateTagDialog} from './update-tag-dialog';\nimport {DataTableAddItemButton} from '../../datatable/data-table-add-item-button';\n\nconst columnConfig: ColumnConfig[] = [\n {\n key: 'name',\n allowsSorting: true,\n visibleInMode: 'all',\n width: 'flex-3 min-w-200',\n header: () => ,\n body: tag => tag.name,\n },\n {\n key: 'type',\n allowsSorting: true,\n header: () => ,\n body: tag => tag.type,\n },\n {\n key: 'display_name',\n allowsSorting: true,\n header: () => ,\n body: tag => tag.display_name,\n },\n {\n key: 'updated_at',\n allowsSorting: true,\n width: 'w-100',\n header: () => ,\n body: tag => ,\n },\n {\n key: 'actions',\n header: () => ,\n hideHeader: true,\n align: 'end',\n width: 'w-42 flex-shrink-0',\n visibleInMode: 'all',\n body: tag => {\n return (\n \n \n \n \n \n \n );\n },\n },\n];\n\nexport function TagIndexPage() {\n const {tags} = useContext(SiteConfigContext);\n const filters = useMemo(() => {\n return TagIndexPageFilters(tags.types);\n }, [tags.types]);\n\n return (\n }\n columns={columnConfig}\n filters={filters}\n actions={}\n selectedActions={}\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n );\n}\n\nfunction Actions() {\n return (\n <>\n \n \n \n \n \n \n \n );\n}\n","import {Fragment, memo} from 'react';\nimport {prettyBytes} from './utils/pretty-bytes';\n\ninterface FormattedBytesProps {\n bytes?: number;\n}\nexport const FormattedBytes = memo(({bytes}: FormattedBytesProps) => {\n return {prettyBytes(bytes)};\n});\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const VisibilityIcon = createSvgIcon(\n \n, 'VisibilityOutlined');\n","export default \"__VITE_ASSET__31048831__\"","import React, {useContext, useMemo} from 'react';\nimport {FileEntry} from '../file-entry';\nimport {useSettings} from '../../core/settings/use-settings';\nimport {isAbsoluteUrl} from '@common/utils/urls/is-absolute-url';\n\nexport const FileEntryUrlsContext = React.createContext<\n Record\n>(null!);\n\nexport function useFileEntryUrls(\n entry?: FileEntry,\n options?: {thumbnail?: boolean; downloadHashes?: string[]},\n): {previewUrl?: string; downloadUrl?: string} {\n const {base_url} = useSettings();\n const urlSearchParams = useContext(FileEntryUrlsContext);\n\n return useMemo(() => {\n if (!entry) {\n return {};\n }\n\n let previewUrl: string | undefined;\n if (entry.url) {\n previewUrl = isAbsoluteUrl(entry.url)\n ? entry.url\n : `${base_url}/${entry.url}`;\n }\n\n const urls = {\n previewUrl,\n downloadUrl: `${base_url}/api/v1/file-entries/download/${\n options?.downloadHashes || entry.hash\n }`,\n };\n\n if (urlSearchParams) {\n // preview url\n if (urls.previewUrl) {\n urls.previewUrl = addParams(\n urls.previewUrl,\n {...urlSearchParams, thumbnail: options?.thumbnail ? 'true' : ''},\n base_url,\n );\n }\n\n // download url\n urls.downloadUrl = addParams(urls.downloadUrl, urlSearchParams, base_url);\n }\n\n return urls;\n }, [\n base_url,\n entry,\n options?.downloadHashes,\n options?.thumbnail,\n urlSearchParams,\n ]);\n}\n\nfunction addParams(urlString: string, params: object, baseUrl: string): string {\n const url = new URL(urlString, baseUrl);\n Object.entries(params).forEach(([key, value]) => {\n url.searchParams.append(key, value as string);\n });\n return url.toString();\n}\n","import React from 'react';\nimport {FileEntry} from '../file-entry';\n\nexport interface FilePreviewContextValue {\n entries: FileEntry[];\n activeIndex: number;\n}\n\nexport const FilePreviewContext = React.createContext(\n null!\n);\n","import {ReactNode, useContext} from 'react';\nimport clsx from 'clsx';\nimport {Button} from '../../../ui/buttons/button';\nimport {downloadFileFromUrl} from '../../utils/download-file-from-url';\nimport {FilePreviewContext} from '../file-preview-context';\nimport {Trans} from '../../../i18n/trans';\nimport {FilePreviewProps} from './file-preview-props';\nimport {useFileEntryUrls} from '../../hooks/file-entry-urls';\n\ninterface Props extends FilePreviewProps {\n message?: ReactNode;\n}\nexport function DefaultFilePreview({message, className, allowDownload}: Props) {\n const {entries, activeIndex} = useContext(FilePreviewContext);\n const activeEntry = entries[activeIndex];\n const content = message || ;\n const {downloadUrl} = useFileEntryUrls(activeEntry);\n return (\n \n
{content}
\n {allowDownload && (\n
\n {\n if (downloadUrl) {\n downloadFileFromUrl(downloadUrl);\n }\n }}\n >\n \n \n
\n )}\n \n );\n}\n","import clsx from 'clsx';\nimport {useFileEntryUrls} from '../../hooks/file-entry-urls';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {FilePreviewProps} from './file-preview-props';\nimport {DefaultFilePreview} from './default-file-preview';\n\nexport function ImageFilePreview(props: FilePreviewProps) {\n const {entry, className} = props;\n const {trans} = useTrans();\n const {previewUrl} = useFileEntryUrls(entry);\n\n if (!previewUrl) {\n return ;\n }\n\n return (\n \n );\n}\n","import {useEffect, useState} from 'react';\nimport clsx from 'clsx';\nimport {FilePreviewProps} from './file-preview-props';\nimport {DefaultFilePreview} from './default-file-preview';\nimport {ProgressCircle} from '@common/ui/progress/progress-circle';\nimport {useFileEntryUrls} from '@common/uploads/hooks/file-entry-urls';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {Trans} from '@common/i18n/trans';\nimport {apiClient} from '@common/http/query-client';\n\nconst FIVE_MB = 5242880;\n\nexport function TextFilePreview(props: FilePreviewProps) {\n const {entry, className} = props;\n const {trans} = useTrans();\n const [tooLarge, setTooLarge] = useState(false);\n const [isLoading, setIsLoading] = useState(true);\n const [isFailed, setIsFailed] = useState(false);\n const [contents, setContents] = useState(null);\n const {previewUrl} = useFileEntryUrls(entry);\n\n useEffect(() => {\n if (!entry) return;\n if (!previewUrl) {\n setIsFailed(true);\n } else if (entry.file_size! >= FIVE_MB) {\n setTooLarge(true);\n setIsLoading(false);\n } else {\n getFileContents(previewUrl)\n .then(response => {\n setContents(response.data);\n })\n .catch(() => {\n setIsFailed(true);\n })\n .finally(() => {\n setIsLoading(false);\n });\n }\n }, [entry, previewUrl]);\n\n if (isLoading) {\n return (\n \n );\n }\n\n if (tooLarge) {\n return (\n }\n />\n );\n }\n\n if (isFailed) {\n return (\n }\n />\n );\n }\n\n return (\n \n
{`${contents}`}
\n \n );\n}\n\nfunction getFileContents(src: string) {\n return apiClient.get(src, {\n responseType: 'text',\n // required for s3 presigned url to work\n withCredentials: false,\n headers: {\n Accept: 'text/plain',\n },\n });\n}\n","import {useEffect, useRef, useState} from 'react';\nimport {FilePreviewProps} from './file-preview-props';\nimport {DefaultFilePreview} from './default-file-preview';\nimport {useFileEntryUrls} from '../../hooks/file-entry-urls';\n\nexport function VideoFilePreview(props: FilePreviewProps) {\n const {entry, className} = props;\n const {previewUrl} = useFileEntryUrls(entry);\n const ref = useRef(null);\n const [mediaInvalid, setMediaInvalid] = useState(false);\n\n useEffect(() => {\n setMediaInvalid(!ref.current?.canPlayType(entry.mime));\n }, [entry]);\n\n if (mediaInvalid || !previewUrl) {\n return ;\n }\n\n return (\n \n {\n setMediaInvalid(true);\n }}\n />\n \n );\n}\n","import {FilePreviewProps} from './file-preview-props';\nimport {DefaultFilePreview} from './default-file-preview';\nimport {useFileEntryUrls} from '../../hooks/file-entry-urls';\nimport {useEffect, useRef, useState} from 'react';\n\nexport function AudioFilePreview(props: FilePreviewProps) {\n const {entry, className} = props;\n const {previewUrl} = useFileEntryUrls(entry);\n const ref = useRef(null);\n const [mediaInvalid, setMediaInvalid] = useState(false);\n\n useEffect(() => {\n setMediaInvalid(!ref.current?.canPlayType(entry.mime));\n }, [entry]);\n\n if (mediaInvalid || !previewUrl) {\n return ;\n }\n\n return (\n \n {\n setMediaInvalid(true);\n }}\n />\n \n );\n}\n","import clsx from 'clsx';\nimport {FilePreviewProps} from './file-preview-props';\nimport {useFileEntryUrls} from '../../hooks/file-entry-urls';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {DefaultFilePreview} from './default-file-preview';\n\nexport function PdfFilePreview(props: FilePreviewProps) {\n const {entry, className} = props;\n const {trans} = useTrans();\n const {previewUrl} = useFileEntryUrls(entry);\n\n if (!previewUrl) {\n return ;\n }\n\n return (\n \n );\n}\n","import clsx from 'clsx';\nimport {useEffect, useRef, useState} from 'react';\nimport {FilePreviewProps} from './file-preview-props';\nimport {DefaultFilePreview} from './default-file-preview';\nimport {ProgressCircle} from '../../../ui/progress/progress-circle';\nimport {FileEntry} from '../../file-entry';\nimport {useFileEntryUrls} from '../../hooks/file-entry-urls';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {apiClient} from '../../../http/query-client';\n\nexport function WordDocumentFilePreview(props: FilePreviewProps) {\n const {entry, className} = props;\n const {trans} = useTrans();\n const ref = useRef(null);\n const [showDefault, setShowDefault] = useState(false);\n const timeoutId = useRef();\n const [isLoading, setIsLoading] = useState(false);\n const {previewUrl} = useFileEntryUrls(entry);\n\n useEffect(() => {\n // Google Docs viewer only supports files up to 25MB\n if (!previewUrl) {\n setShowDefault(true);\n } else if (entry.file_size && entry.file_size > 25000000) {\n setShowDefault(true);\n } else if (ref.current) {\n ref.current.onload = () => {\n clearTimeout(timeoutId.current);\n setIsLoading(false);\n };\n\n buildPreviewUrl(previewUrl, entry).then(url => {\n if (ref.current) {\n ref.current.src = url;\n }\n });\n\n // if preview iframe is not loaded\n // after 5 seconds, bail and show default preview\n timeoutId.current = setTimeout(() => {\n setShowDefault(true);\n }, 5000);\n }\n }, [entry, previewUrl]);\n\n if (showDefault) {\n return ;\n }\n\n return (\n
\n {isLoading && }\n \n
\n );\n}\n\nasync function buildPreviewUrl(\n urlString: string,\n entry: FileEntry\n): Promise {\n const url = new URL(urlString);\n // if we're not trying to preview shareable link we will need to generate\n // preview token, otherwise it won't be publicly accessible\n if (!url.searchParams.has('shareable_link')) {\n const {data} = await apiClient.post(\n `file-entries/${entry.id}/add-preview-token`\n );\n url.searchParams.append('preview_token', data.preview_token);\n }\n\n return buildOfficeLivePreviewUrl(url);\n}\n\nfunction buildOfficeLivePreviewUrl(url: URL) {\n // https://docs.google.com/gview?embedded=true&url=\n return `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(\n url.toString()\n )}`;\n}\n","import {ImageFilePreview} from './file-preview/image-file-preview';\nimport {FileEntry} from '../file-entry';\nimport {DefaultFilePreview} from './file-preview/default-file-preview';\nimport {TextFilePreview} from './file-preview/text-file-preview';\nimport {VideoFilePreview} from './file-preview/video-file-preview';\nimport {AudioFilePreview} from './file-preview/audio-file-preview';\nimport {PdfFilePreview} from './file-preview/pdf-file-preview';\nimport {WordDocumentFilePreview} from './file-preview/word-document-file-preview';\n\nexport const AvailablePreviews = {\n text: TextFilePreview,\n video: VideoFilePreview,\n audio: AudioFilePreview,\n image: ImageFilePreview,\n pdf: PdfFilePreview,\n spreadsheet: WordDocumentFilePreview,\n powerPoint: WordDocumentFilePreview,\n word: WordDocumentFilePreview,\n 'text/rtf': DefaultFilePreview,\n} as const;\n\nexport function getPreviewForEntry(entry: FileEntry) {\n const mime = entry?.mime as keyof typeof AvailablePreviews;\n const type = entry?.type as keyof typeof AvailablePreviews;\n return (\n AvailablePreviews[mime] || AvailablePreviews[type] || DefaultFilePreview\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 clsx from 'clsx';\nimport {FileTypeIcon} from './file-type-icon';\nimport {useFileEntryUrls} from '../hooks/file-entry-urls';\nimport {useTrans} from '../../i18n/use-trans';\nimport {FileEntry} from '../file-entry';\n\nconst TwoMB = 2 * 1024 * 1024;\n\ninterface Props {\n file: FileEntry;\n className?: string;\n iconClassName?: string;\n showImage?: boolean;\n}\nexport function FileThumbnail({\n file,\n className,\n iconClassName,\n showImage = true,\n}: Props) {\n const {trans} = useTrans();\n const {previewUrl} = useFileEntryUrls(file, {thumbnail: true});\n\n // don't show images for files larger than 2MB, if thumbnail was not generated to avoid ui lag\n if (file.file_size && file.file_size > TwoMB && !file.thumbnail) {\n showImage = false;\n }\n\n if (showImage && file.type === 'image' && previewUrl) {\n const alt = trans({\n message: ':fileName thumbnail',\n values: {fileName: file.name},\n });\n return (\n \n );\n }\n return ;\n}\n","import {AnimatePresence, m} from 'framer-motion';\nimport {Fragment, ReactNode, useContext, useMemo} from 'react';\nimport clsx from 'clsx';\nimport {getPreviewForEntry} from './available-previews';\nimport {FileEntry} from '../file-entry';\nimport {FilePreviewContext} from './file-preview-context';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {ChevronLeftIcon} from '../../icons/material/ChevronLeft';\nimport {ChevronRightIcon} from '../../icons/material/ChevronRight';\nimport {FileDownloadIcon} from '../../icons/material/FileDownload';\nimport {downloadFileFromUrl} from '../utils/download-file-from-url';\nimport {useFileEntryUrls} from '../hooks/file-entry-urls';\nimport {Trans} from '../../i18n/trans';\nimport {Button} from '../../ui/buttons/button';\nimport {CloseIcon} from '../../icons/material/Close';\nimport {FileThumbnail} from '../file-type-icon/file-thumbnail';\nimport {useMediaQuery} from '../../utils/hooks/use-media-query';\nimport {KeyboardArrowLeftIcon} from '../../icons/material/KeyboardArrowLeft';\nimport {KeyboardArrowRightIcon} from '../../icons/material/KeyboardArrowRight';\nimport {useControlledState} from '@react-stately/utils';\nimport {opacityAnimation} from '../../ui/animation/opacity-animation';\n\nexport interface FilePreviewContainerProps {\n entries: FileEntry[];\n activeIndex?: number;\n defaultActiveIndex?: number;\n onActiveIndexChange?: (index: number) => void;\n onClose?: () => void;\n showHeader?: boolean;\n headerActionsLeft?: ReactNode;\n className?: string;\n allowDownload?: boolean;\n}\nexport function FilePreviewContainer({\n entries,\n onClose,\n showHeader = true,\n className,\n headerActionsLeft,\n allowDownload = true,\n ...props\n}: FilePreviewContainerProps) {\n const isMobile = useMediaQuery('(max-width: 1024px)');\n\n const [activeIndex, setActiveIndex] = useControlledState(\n props.activeIndex,\n props.defaultActiveIndex || 0,\n props.onActiveIndexChange\n );\n\n const activeEntry = entries[activeIndex];\n const contextValue = useMemo(() => {\n return {entries, activeIndex};\n }, [entries, activeIndex]);\n const Preview = getPreviewForEntry(activeEntry);\n\n if (!activeEntry) {\n onClose?.();\n return null;\n }\n\n const canOpenNext = entries.length - 1 > activeIndex;\n const openNext = () => {\n setActiveIndex(activeIndex + 1);\n };\n const canOpenPrevious = activeIndex > 0;\n const openPrevious = () => {\n setActiveIndex(activeIndex - 1);\n };\n\n return (\n \n {showHeader && (\n \n )}\n
\n {isMobile && (\n \n \n \n )}\n \n \n \n \n \n {isMobile && (\n \n \n \n )}\n
\n
\n );\n}\n\ninterface HeaderProps {\n onNext?: () => void;\n onPrevious?: () => void;\n onClose?: () => void;\n isMobile: boolean | null;\n actionsLeft?: ReactNode;\n allowDownload?: boolean;\n}\nfunction Header({\n onNext,\n onPrevious,\n onClose,\n isMobile,\n actionsLeft,\n allowDownload,\n}: HeaderProps) {\n const {entries, activeIndex} = useContext(FilePreviewContext);\n const activeEntry = entries[activeIndex];\n const {downloadUrl} = useFileEntryUrls(activeEntry);\n\n const desktopDownloadButton = (\n }\n variant=\"text\"\n onClick={() => {\n if (downloadUrl) {\n downloadFileFromUrl(downloadUrl);\n }\n }}\n >\n \n \n );\n\n const mobileDownloadButton = (\n {\n if (downloadUrl) {\n downloadFileFromUrl(downloadUrl);\n }\n }}\n >\n \n \n );\n\n const downloadButton = isMobile\n ? mobileDownloadButton\n : desktopDownloadButton;\n\n return (\n
\n
\n {actionsLeft}\n {allowDownload ? downloadButton : undefined}\n
\n
\n \n
\n {activeEntry.name}\n
\n
\n
\n {!isMobile && (\n \n \n \n \n
{activeIndex + 1}
\n
/
\n
{entries.length}
\n \n \n \n
\n \n )}\n \n \n \n
\n
\n );\n}\n","import {\n FilePreviewContainer,\n FilePreviewContainerProps,\n} from './file-preview-container';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\nimport {Dialog} from '../../ui/overlays/dialog/dialog';\n\ninterface Props extends Omit {}\nexport function FilePreviewDialog(props: Props) {\n return (\n \n \n \n );\n}\n\nfunction Content(props: Props) {\n const {close} = useDialogContext();\n return ;\n}\n","import {\n BackendFilter,\n FilterControlType,\n FilterOperator,\n FilterSelectControl,\n} from '../../datatable/filters/backend-filter';\nimport {message} from '../../i18n/message';\nimport {USER_MODEL} from '../../auth/user';\nimport {\n createdAtFilter,\n updatedAtFilter,\n} from '@common/datatable/filters/timestamp-filters';\n\nexport const FILE_ENTRY_TYPE_FILTER: BackendFilter = {\n key: 'type',\n label: message('Type'),\n description: message('Type of the file'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.Select,\n defaultValue: '05',\n options: [\n {key: '02', label: message('Text'), value: 'text'},\n {\n key: '03',\n label: message('Audio'),\n value: 'audio',\n },\n {\n key: '04',\n label: message('Video'),\n value: 'video',\n },\n {\n key: '05',\n label: message('Image'),\n value: 'image',\n },\n {key: '06', label: message('PDF'), value: 'pdf'},\n {\n key: '07',\n label: message('Spreadsheet'),\n value: 'spreadsheet',\n },\n {\n key: '08',\n label: message('Word Document'),\n value: 'word',\n },\n {\n key: '09',\n label: message('Photoshop'),\n value: 'photoshop',\n },\n {\n key: '10',\n label: message('Archive'),\n value: 'archive',\n },\n {\n key: '11',\n label: message('Folder'),\n value: 'folder',\n },\n ],\n },\n};\n\nexport const FILE_ENTRY_INDEX_FILTERS: BackendFilter[] = [\n FILE_ENTRY_TYPE_FILTER,\n {\n key: 'public',\n label: message('Visibility'),\n description: message('Whether file is publicly accessible'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.Select,\n defaultValue: '01',\n options: [\n {key: '01', label: message('Private'), value: false},\n {key: '02', label: message('Public'), value: true},\n ],\n },\n },\n createdAtFilter({\n description: message('Date file was uploaded'),\n }),\n updatedAtFilter({\n description: message('Date file was last changed'),\n }),\n {\n key: 'owner_id',\n label: message('Uploader'),\n description: message('User that this file was uploaded by'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.SelectModel,\n model: USER_MODEL,\n },\n },\n];\n","import React, {Fragment} from 'react';\nimport {DataTablePage} from '../../datatable/page/data-table-page';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {FormattedDate} from '../../i18n/formatted-date';\nimport {ColumnConfig} from '../../datatable/column-config';\nimport {Trans} from '../../i18n/trans';\nimport {DeleteSelectedItemsAction} from '../../datatable/page/delete-selected-items-action';\nimport {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {FileEntry} from '../../uploads/file-entry';\nimport {NameWithAvatar} from '../../datatable/column-templates/name-with-avatar';\nimport {User} from '../../auth/user';\nimport {CheckIcon} from '../../icons/material/Check';\nimport {CloseIcon} from '../../icons/material/Close';\nimport {FormattedBytes} from '../../uploads/formatted-bytes';\nimport {VisibilityIcon} from '../../icons/material/Visibility';\nimport uploadSvg from './upload.svg';\nimport {FilePreviewDialog} from '../../uploads/preview/file-preview-dialog';\nimport {FILE_ENTRY_INDEX_FILTERS} from './file-entry-index-filters';\nimport {FileTypeIcon} from '../../uploads/file-type-icon/file-type-icon';\n\nconst columnConfig: ColumnConfig[] = [\n {\n key: 'name',\n allowsSorting: true,\n visibleInMode: 'all',\n width: 'flex-3 min-w-200',\n header: () => ,\n body: entry => (\n \n
{entry.name}
\n
\n {entry.file_name}\n
\n
\n ),\n },\n {\n key: 'owner_id',\n allowsSorting: true,\n width: 'flex-3 min-w-200',\n header: () => ,\n body: entry => {\n const user = entry.users?.[0] as User;\n if (!user) return null;\n return (\n \n );\n },\n },\n {\n key: 'type',\n width: 'w-100 flex-shrink-0',\n allowsSorting: true,\n header: () => ,\n body: entry => (\n
\n \n
{entry.type}
\n
\n ),\n },\n {\n key: 'public',\n allowsSorting: true,\n width: 'w-60 flex-shrink-0',\n header: () => ,\n body: entry =>\n entry.public ? (\n \n ) : (\n \n ),\n },\n {\n key: 'file_size',\n allowsSorting: true,\n maxWidth: 'max-w-100',\n header: () => ,\n body: entry => ,\n },\n {\n key: 'updated_at',\n allowsSorting: true,\n width: 'w-100',\n header: () => ,\n body: entry => ,\n },\n {\n key: 'actions',\n header: () => ,\n hideHeader: true,\n align: 'end',\n width: 'w-42 flex-shrink-0',\n visibleInMode: 'all',\n body: entry => {\n return (\n \n \n \n \n \n \n );\n },\n },\n];\n\nexport function FileEntryIndexPage() {\n return (\n }\n columns={columnConfig}\n filters={FILE_ENTRY_INDEX_FILTERS}\n selectedActions={}\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n );\n}\n","import {\n BackendFilter,\n FilterControlType,\n FilterOperator,\n} from '../../datatable/filters/backend-filter';\nimport {message} from '../../i18n/message';\nimport {\n createdAtFilter,\n timestampFilter,\n updatedAtFilter,\n} from '../../datatable/filters/timestamp-filters';\n\nexport const SubscriptionIndexPageFilters: BackendFilter[] = [\n {\n key: 'ends_at',\n label: message('Status'),\n description: message('Whether subscription is active or cancelled'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.Select,\n defaultValue: 'active',\n options: [\n {\n key: 'active',\n label: message('Active'),\n value: {value: null, operator: FilterOperator.eq},\n },\n {\n key: 'cancelled',\n label: message('Cancelled'),\n value: {value: null, operator: FilterOperator.ne},\n },\n ],\n },\n },\n {\n control: {\n type: FilterControlType.Select,\n defaultValue: 'stripe',\n options: [\n {\n key: 'stripe',\n label: message('Stripe'),\n value: 'stripe',\n },\n {\n key: 'paypal',\n label: message('PayPal'),\n value: 'paypal',\n },\n {\n key: 'none',\n label: message('None'),\n value: 'none',\n },\n ],\n },\n key: 'gateway_name',\n label: message('Gateway'),\n description: message(\n 'With which payment provider was subscription created'\n ),\n defaultOperator: FilterOperator.eq,\n },\n timestampFilter({\n key: 'renews_at',\n label: message('Renew date'),\n description: message('Date subscription will renew'),\n }),\n createdAtFilter({\n description: message('Date subscription was created'),\n }),\n updatedAtFilter({\n description: message('Date subscription was last updated'),\n }),\n];\n","export default \"__VITE_ASSET__2e46d67b__\"","import {useMutation} from '@tanstack/react-query';\nimport {apiClient, queryClient} from '../../../http/query-client';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {message} from '../../../i18n/message';\nimport {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';\nimport {onFormQueryError} from '../../../errors/on-form-query-error';\nimport {UseFormReturn} from 'react-hook-form';\nimport {Subscription} from '../../../billing/subscription';\n\ninterface Response extends BackendResponse {\n subscription: Subscription;\n}\n\nexport interface UpdateSubscriptionPayload extends Partial {\n id: number;\n}\n\nexport function useUpdateSubscription(\n form: UseFormReturn,\n) {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: (props: UpdateSubscriptionPayload) => updateSubscription(props),\n onSuccess: () => {\n toast(trans(message('Subscription updated')));\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('billing/subscriptions'),\n });\n },\n onError: err => onFormQueryError(err, form),\n });\n}\n\nfunction updateSubscription({\n id,\n ...payload\n}: UpdateSubscriptionPayload): Promise {\n return apiClient\n .put(`billing/subscriptions/${id}`, payload)\n .then(r => r.data);\n}\n","import {UseFormReturn} from 'react-hook-form';\nimport {Form} from '../../ui/forms/form';\nimport {FormTextField} from '../../ui/forms/input-field/text-field/text-field';\nimport {FormSelect} from '../../ui/forms/select/select';\nimport {Trans} from '../../i18n/trans';\nimport {Item} from '../../ui/forms/listbox/item';\nimport {Subscription} from '../../billing/subscription';\nimport {FormDatePicker} from '../../ui/forms/input-field/date/date-picker/date-picker';\nimport {useProducts} from '../../billing/pricing-table/use-products';\nimport {FormattedPrice} from '../../i18n/formatted-price';\nimport {FormNormalizedModelField} from '../../ui/forms/normalized-model-field';\n\ninterface CrupdateSubscriptionForm {\n onSubmit: (values: Partial) => void;\n formId: string;\n form: UseFormReturn>;\n}\nexport function CrupdateSubscriptionForm({\n form,\n onSubmit,\n formId,\n}: CrupdateSubscriptionForm) {\n const query = useProducts();\n // @ts-ignore\n const watchedProductId = form.watch('product_id');\n const selectedProduct = query.data?.products.find(\n p => p.id === watchedProductId,\n );\n\n return (\n
\n }\n />\n }\n >\n {query.data?.products\n .filter(p => !p.free)\n .map(product => (\n \n \n \n ))}\n \n {!selectedProduct?.free && (\n }\n >\n {selectedProduct?.prices.map(price => (\n \n \n \n ))}\n \n )}\n }\n className=\"mb-20\"\n />\n }\n description={\n \n }\n />\n }\n description={\n \n }\n />\n \n );\n}\n","import {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {Trans} from '../../i18n/trans';\nimport {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';\nimport {Button} from '../../ui/buttons/button';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\nimport {useForm} from 'react-hook-form';\nimport {Subscription} from '../../billing/subscription';\nimport {\n UpdateSubscriptionPayload,\n useUpdateSubscription,\n} from './requests/use-update-subscription';\nimport {CrupdateSubscriptionForm} from './crupdate-subscription-form';\n\ninterface UpdateSubscriptionDialogProps {\n subscription: Subscription;\n}\nexport function UpdateSubscriptionDialog({\n subscription,\n}: UpdateSubscriptionDialogProps) {\n const {close, formId} = useDialogContext();\n const form = useForm({\n defaultValues: {\n id: subscription.id,\n product_id: subscription.product_id,\n price_id: subscription.price_id,\n description: subscription.description,\n renews_at: subscription.renews_at,\n ends_at: subscription.ends_at,\n user_id: subscription.user_id,\n },\n });\n const updateSubscription = useUpdateSubscription(form);\n\n return (\n \n \n \n \n \n {\n updateSubscription.mutate(values as UpdateSubscriptionPayload, {\n onSuccess: () => {\n close();\n },\n });\n }}\n />\n \n \n {\n close();\n }}\n >\n \n \n \n \n \n \n \n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient, queryClient} from '../../../http/query-client';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {message} from '../../../i18n/message';\nimport {Tag} from '../../../tags/tag';\nimport {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';\nimport {onFormQueryError} from '../../../errors/on-form-query-error';\nimport {UseFormReturn} from 'react-hook-form';\nimport {Subscription} from '../../../billing/subscription';\n\nconst endpoint = 'billing/subscriptions';\n\ninterface Response extends BackendResponse {\n tag: Tag;\n}\n\ninterface Payload extends Partial {}\n\nexport function useCreateSubscription(form: UseFormReturn) {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: (props: Payload) => createNewSubscription(props),\n onSuccess: () => {\n toast(trans(message('Subscription created')));\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey(endpoint),\n });\n },\n onError: err => onFormQueryError(err, form),\n });\n}\n\nfunction createNewSubscription(payload: Payload): Promise {\n return apiClient.post(endpoint, payload).then(r => r.data);\n}\n","import {Dialog} from '../../ui/overlays/dialog/dialog';\nimport {DialogHeader} from '../../ui/overlays/dialog/dialog-header';\nimport {Trans} from '../../i18n/trans';\nimport {DialogBody} from '../../ui/overlays/dialog/dialog-body';\nimport {DialogFooter} from '../../ui/overlays/dialog/dialog-footer';\nimport {Button} from '../../ui/buttons/button';\nimport {useDialogContext} from '../../ui/overlays/dialog/dialog-context';\nimport {useForm} from 'react-hook-form';\nimport {useCreateSubscription} from './requests/use-create-subscription';\nimport {Subscription} from '../../billing/subscription';\nimport {CrupdateSubscriptionForm} from './crupdate-subscription-form';\n\nexport function CreateSubscriptionDialog() {\n const {close, formId} = useDialogContext();\n const form = useForm>({});\n const createSubscription = useCreateSubscription(form);\n\n return (\n \n \n \n \n \n {\n createSubscription.mutate(values, {\n onSuccess: () => {\n close();\n },\n });\n }}\n />\n \n \n {\n close();\n }}\n >\n \n \n \n \n \n \n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const PauseIcon = createSvgIcon(\n \n, 'PauseOutlined');\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const PlayArrowIcon = createSvgIcon(\n \n, 'PlayArrowOutlined');\n","import React, {Fragment} from 'react';\nimport {DataTablePage} from '../../datatable/page/data-table-page';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {EditIcon} from '../../icons/material/Edit';\nimport {ColumnConfig} from '../../datatable/column-config';\nimport {Trans} from '../../i18n/trans';\nimport {DeleteSelectedItemsAction} from '../../datatable/page/delete-selected-items-action';\nimport {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';\nimport {SubscriptionIndexPageFilters} from './subscription-index-page-filters';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {DataTableAddItemButton} from '../../datatable/data-table-add-item-button';\nimport subscriptionsSvg from './subscriptions.svg';\nimport {NameWithAvatar} from '../../datatable/column-templates/name-with-avatar';\nimport {Subscription} from '../../billing/subscription';\nimport {CloseIcon} from '../../icons/material/Close';\nimport {FormattedDate} from '../../i18n/formatted-date';\nimport {UpdateSubscriptionDialog} from './update-subscription-dialog';\nimport {CreateSubscriptionDialog} from './create-subscription-dialog';\nimport {useCancelSubscription} from '../../billing/billing-page/requests/use-cancel-subscription';\nimport {PauseIcon} from '../../icons/material/Pause';\nimport {queryClient} from '../../http/query-client';\nimport {DatatableDataQueryKey} from '../../datatable/requests/paginated-resources';\nimport {Tooltip} from '../../ui/tooltip/tooltip';\nimport {useResumeSubscription} from '../../billing/billing-page/requests/use-resume-subscription';\nimport {PlayArrowIcon} from '../../icons/material/PlayArrow';\nimport {ConfirmationDialog} from '../../ui/overlays/dialog/confirmation-dialog';\nimport {Chip} from '../../ui/forms/input-field/chip-field/chip';\n\nconst endpoint = 'billing/subscriptions';\n\nconst columnConfig: ColumnConfig[] = [\n {\n key: 'user_id',\n allowsSorting: true,\n width: 'flex-3 min-w-200',\n visibleInMode: 'all',\n header: () => ,\n body: subscription =>\n subscription.user && (\n \n ),\n },\n {\n key: 'status',\n width: 'w-100 flex-shrink-0',\n header: () => ,\n body: subscription => (\n \n {subscription.gateway_status}\n \n ),\n },\n {\n key: 'product_id',\n allowsSorting: true,\n header: () => ,\n body: subscription => subscription.product?.name,\n },\n {\n key: 'gateway_name',\n allowsSorting: true,\n header: () => ,\n body: subscription => (\n {subscription.gateway_name}\n ),\n },\n {\n key: 'renews_at',\n allowsSorting: true,\n header: () => ,\n body: subscription => ,\n },\n {\n key: 'ends_at',\n allowsSorting: true,\n header: () => ,\n body: subscription => ,\n },\n {\n key: 'created_at',\n allowsSorting: true,\n header: () => ,\n body: subscription => ,\n },\n {\n key: 'actions',\n header: () => ,\n hideHeader: true,\n align: 'end',\n visibleInMode: 'all',\n width: 'w-[168px] flex-shrink-0',\n body: subscription => {\n return ;\n },\n },\n];\n\nexport function SubscriptionsIndexPage() {\n return (\n }\n columns={columnConfig}\n filters={SubscriptionIndexPageFilters}\n actions={}\n enableSelection={false}\n selectedActions={}\n queryParams={{with: 'product'}}\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n );\n}\n\nfunction PageActions() {\n return (\n <>\n \n \n \n \n \n \n \n );\n}\n\ninterface SubscriptionActionsProps {\n subscription: Subscription;\n}\nfunction SubscriptionActions({subscription}: SubscriptionActionsProps) {\n return (\n \n \n \n \n \n \n \n {subscription.cancelled && subscription.on_grace_period ? (\n \n ) : null}\n {subscription.active ? (\n \n ) : null}\n \n \n );\n}\n\nfunction SuspendSubscriptionButton({subscription}: SubscriptionActionsProps) {\n const cancelSubscription = useCancelSubscription();\n\n const handleSuspendSubscription = () => {\n cancelSubscription.mutate(\n {subscriptionId: subscription.id},\n {\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey(endpoint),\n });\n },\n },\n );\n };\n\n return (\n {\n if (confirmed) {\n handleSuspendSubscription();\n }\n }}\n >\n }>\n \n \n \n \n }\n body={\n
\n \n
\n \n
\n
\n }\n confirm={}\n />\n \n );\n}\n\nfunction ResumeSubscriptionButton({subscription}: SubscriptionActionsProps) {\n const resumeSubscription = useResumeSubscription();\n const handleResumeSubscription = () => {\n resumeSubscription.mutate(\n {subscriptionId: subscription.id},\n {\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey(endpoint),\n });\n },\n },\n );\n };\n\n return (\n {\n if (confirmed) {\n handleResumeSubscription();\n }\n }}\n >\n }>\n \n \n \n \n }\n body={\n
\n \n
\n \n
\n
\n }\n confirm={}\n />\n \n );\n}\n\nfunction CancelSubscriptionButton({subscription}: SubscriptionActionsProps) {\n const cancelSubscription = useCancelSubscription();\n\n const handleDeleteSubscription = () => {\n cancelSubscription.mutate(\n {subscriptionId: subscription.id, delete: true},\n {\n onSuccess: () => {\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey(endpoint),\n });\n },\n },\n );\n };\n\n return (\n {\n if (confirmed) {\n handleDeleteSubscription();\n }\n }}\n >\n }>\n \n \n \n \n }\n body={\n
\n \n
\n \n
\n
\n }\n confirm={}\n />\n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const SyncIcon = createSvgIcon(\n \n, 'SyncOutlined');\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient} from '../../../http/query-client';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {message} from '../../../i18n/message';\nimport {showHttpErrorToast} from '../../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\nexport function useSyncProducts() {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: () => syncPlans(),\n onSuccess: () => {\n toast(trans(message('Plans synced')));\n },\n onError: err => showHttpErrorToast(err, message('Could not sync plans')),\n });\n}\n\nfunction syncPlans(): Promise {\n return apiClient.post('billing/products/sync').then(r => r.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient, queryClient} from '../../../http/query-client';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {message} from '../../../i18n/message';\nimport {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';\nimport {showHttpErrorToast} from '../../../utils/http/show-http-error-toast';\n\nconst endpoint = (id: number) => `billing/products/${id}`;\n\ninterface Response extends BackendResponse {}\n\ninterface Payload {\n productId: number;\n}\n\nexport function useDeleteProduct() {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: (payload: Payload) => updateProduct(payload),\n onSuccess: () => {\n toast(trans(message('Plan deleted')));\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('billing/products'),\n });\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction updateProduct({productId}: Payload): Promise {\n return apiClient.delete(endpoint(productId)).then(r => r.data);\n}\n","import {\n BackendFilter,\n FilterControlType,\n FilterOperator,\n} from '../../datatable/filters/backend-filter';\nimport {message} from '../../i18n/message';\nimport {\n createdAtFilter,\n updatedAtFilter,\n} from '@common/datatable/filters/timestamp-filters';\n\nexport const PlansIndexPageFilters: BackendFilter[] = [\n {\n key: 'subscriptions',\n label: message('Subscriptions'),\n description: message('Whether plan has any active subscriptions'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.Select,\n defaultValue: '01',\n options: [\n {\n key: '01',\n label: message('Has active subscriptions'),\n value: {value: '*', operator: FilterOperator.has},\n },\n {\n key: '02',\n label: message('Does not have active subscriptions'),\n value: {value: '*', operator: FilterOperator.doesntHave},\n },\n ],\n },\n },\n createdAtFilter({\n description: message('Date plan was created'),\n }),\n updatedAtFilter({\n description: message('Date plan was last updated'),\n }),\n];\n","import React, {Fragment} from 'react';\nimport {DataTablePage} from '../../datatable/page/data-table-page';\nimport {IconButton} from '../../ui/buttons/icon-button';\nimport {EditIcon} from '../../icons/material/Edit';\nimport {FormattedDate} from '../../i18n/formatted-date';\nimport {ColumnConfig} from '../../datatable/column-config';\nimport {Trans} from '../../i18n/trans';\nimport {DataTableEmptyStateMessage} from '../../datatable/page/data-table-emty-state-message';\nimport softwareEngineerSvg from './../tags/software-engineer.svg';\nimport {DataTableAddItemButton} from '../../datatable/data-table-add-item-button';\nimport {Product} from '../../billing/product';\nimport {NameWithAvatar} from '../../datatable/column-templates/name-with-avatar';\nimport {Link} from 'react-router-dom';\nimport {FormattedPrice} from '../../i18n/formatted-price';\nimport {SyncIcon} from '../../icons/material/Sync';\nimport {useSyncProducts} from './requests/use-sync-products';\nimport {Tooltip} from '../../ui/tooltip/tooltip';\nimport {useDeleteProduct} from './requests/use-delete-product';\nimport {DeleteIcon} from '../../icons/material/Delete';\nimport {DialogTrigger} from '../../ui/overlays/dialog/dialog-trigger';\nimport {ConfirmationDialog} from '../../ui/overlays/dialog/confirmation-dialog';\nimport {useNavigate} from '../../utils/hooks/use-navigate';\nimport {PlansIndexPageFilters} from './plans-index-page-filters';\n\nconst columnConfig: ColumnConfig[] = [\n {\n key: 'name',\n allowsSorting: true,\n visibleInMode: 'all',\n header: () => ,\n body: product => {\n const price = product.prices[0];\n return (\n \n ) : (\n \n )\n }\n />\n );\n },\n },\n {\n key: 'created_at',\n allowsSorting: true,\n maxWidth: 'max-w-100',\n header: () => ,\n body: product => ,\n },\n {\n key: 'updated_at',\n allowsSorting: true,\n maxWidth: 'max-w-100',\n header: () => ,\n body: product => ,\n },\n {\n key: 'actions',\n header: () => ,\n visibleInMode: 'all',\n hideHeader: true,\n align: 'end',\n maxWidth: 'max-w-84',\n body: product => {\n return (\n \n \n \n \n \n \n );\n },\n },\n];\n\nexport function PlansIndexPage() {\n const navigate = useNavigate();\n return (\n }\n columns={columnConfig}\n actions={}\n enableSelection={false}\n filters={PlansIndexPageFilters}\n onRowAction={item => {\n navigate(`/admin/plans/${item.id}/edit`);\n }}\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n );\n}\n\ninterface DeleteProductButtonProps {\n product: Product;\n}\nfunction DeleteProductButton({product}: DeleteProductButtonProps) {\n const deleteProduct = useDeleteProduct();\n return (\n {\n if (confirmed) {\n deleteProduct.mutate({productId: product.id});\n }\n }}\n >\n }>\n \n \n \n \n }\n body={}\n confirm={}\n />\n \n );\n}\n\nfunction Actions() {\n const syncPlans = useSyncProducts();\n return (\n \n }>\n {\n syncPlans.mutate();\n }}\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';\nimport {useParams} from 'react-router-dom';\nimport {Product} from '@common/billing/product';\n\nconst Endpoint = (id: number | string) => `billing/products/${id}`;\n\nexport interface FetchRoleResponse extends BackendResponse {\n product: Product;\n}\n\nexport function useProduct() {\n const {productId} = useParams();\n return useQuery({\n queryKey: [Endpoint(productId!)],\n queryFn: () => fetchProduct(productId!),\n });\n}\n\nfunction fetchProduct(productId: number | string): Promise {\n return apiClient.get(Endpoint(productId)).then(response => response.data);\n}\n","import {message} from '../../../i18n/message';\n\nexport const BillingPeriodPresets = [\n {\n key: 'day1',\n label: message('Daily'),\n interval: 'day',\n interval_count: 1,\n },\n {\n key: 'week1',\n label: message('Weekly'),\n interval: 'week',\n interval_count: 1,\n },\n {\n key: 'month1',\n label: message('Monthly'),\n interval: 'month',\n interval_count: 1,\n },\n {\n key: 'month3',\n label: message('Every 3 months'),\n interval: 'month',\n interval_count: 3,\n },\n {\n key: 'month6',\n label: message('Every 6 months'),\n interval: 'month',\n interval_count: 6,\n },\n {\n key: 'year1',\n label: message('Yearly'),\n interval: 'year',\n interval_count: 1,\n },\n {\n key: 'custom',\n label: message('Custom'),\n interval: null,\n interval_count: null,\n },\n];\n","import {useFormContext} from 'react-hook-form';\nimport {Product} from '@common/billing/product';\nimport React, {Fragment, useMemo, useState} from 'react';\nimport {useValueLists} from '@common/http/value-lists';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {Trans} from '@common/i18n/trans';\nimport {Item} from '@common/ui/forms/listbox/item';\nimport {FormSelect, Select} from '@common/ui/forms/select/select';\nimport {Price} from '@common/billing/price';\nimport {BillingPeriodPresets} from '@common/admin/plans/crupdate-plan-page/billing-period-presets';\nimport {Button} from '@common/ui/buttons/button';\nimport {message} from '@common/i18n/message';\nimport {useTrans} from '@common/i18n/use-trans';\n\ninterface PriceFormProps {\n index: number;\n onRemovePrice: () => void;\n}\nexport function PriceForm({index, onRemovePrice}: PriceFormProps) {\n const {trans} = useTrans();\n const query = useValueLists(['currencies']);\n const currencies = useMemo(() => {\n return query.data?.currencies ? Object.values(query.data.currencies) : [];\n }, [query.data]);\n const {watch, getValues} = useFormContext();\n const isNewProduct = !watch('id');\n const isNewPrice = watch(`prices.${index}.id`) == null;\n const subscriberCount = watch(`prices.${index}.subscriptions_count`) || 0;\n\n // select billing period preset based on price \"interval\" and \"interval_count\"\n const [billingPeriodPreset, setBillingPeriodPreset] = useState(() => {\n const interval = getValues(`prices.${index}.interval`);\n const intervalCount = getValues(`prices.${index}.interval_count`);\n const preset = BillingPeriodPresets.find(\n p => p.key === `${interval}${intervalCount}`\n );\n return preset ? preset.key : 'custom';\n });\n\n const allowPriceChanges = isNewProduct || isNewPrice || !subscriberCount;\n\n return (\n \n {!allowPriceChanges && (\n

\n \n

\n )}\n\n }\n type=\"number\"\n min={0.1}\n step={0.01}\n name={`prices.${index}.amount`}\n className=\"mb-20\"\n />\n }\n name={`prices.${index}.currency`}\n items={currencies}\n showSearchField\n searchPlaceholder={trans(message('Search currencies'))}\n selectionMode=\"single\"\n className=\"mb-20\"\n >\n {item => (\n {`${item.code}: ${item.name}`}\n )}\n \n \n {billingPeriodPreset === 'custom' && (\n \n )}\n
\n {\n onRemovePrice();\n }}\n >\n \n \n
\n
\n );\n}\n\ninterface BillingPeriodSelectProps {\n index: number;\n value: string;\n onValueChange: (value: string) => void;\n disabled: boolean;\n}\nfunction BillingPeriodSelect({\n index,\n value,\n onValueChange,\n disabled,\n}: BillingPeriodSelectProps) {\n const {setValue: setFormValue} = useFormContext();\n\n return (\n }\n disabled={disabled}\n className=\"mb-20\"\n selectionMode=\"single\"\n selectedValue={value}\n onSelectionChange={value => {\n onValueChange(value as string);\n if (value === 'custom') {\n } else {\n const preset = BillingPeriodPresets.find(p => p.key === value);\n if (preset) {\n setFormValue(\n `prices.${index}.interval`,\n preset.interval as Price['interval']\n );\n setFormValue(\n `prices.${index}.interval_count`,\n preset.interval_count as number\n );\n }\n }\n }}\n >\n {BillingPeriodPresets.map(preset => (\n \n \n \n ))}\n \n );\n}\n\ninterface CustomBillingPeriodFieldProps {\n index: number;\n disabled: boolean;\n}\nfunction CustomBillingPeriodField({\n index,\n disabled,\n}: CustomBillingPeriodFieldProps) {\n const {watch} = useFormContext();\n const interval = watch(`prices.${index}.interval`);\n let maxIntervalCount: number;\n\n if (interval === 'day') {\n maxIntervalCount = 365;\n } else if (interval === 'week') {\n maxIntervalCount = 52;\n } else {\n maxIntervalCount = 12;\n }\n\n return (\n
\n
\n \n
\n \n \n \n \n \n \n \n \n \n \n \n \n
\n );\n}\n","import {FormTextField} from '../../../ui/forms/input-field/text-field/text-field';\nimport {Trans} from '../../../i18n/trans';\nimport React, {Fragment, ReactNode} from 'react';\nimport {useFieldArray, useFormContext} from 'react-hook-form';\nimport {Accordion, AccordionItem} from '../../../ui/accordion/accordion';\nimport {FormattedPrice} from '../../../i18n/formatted-price';\nimport {FormPermissionSelector} from '../../../auth/ui/permission-selector';\nimport {PriceForm} from './price-form';\nimport {Button} from '../../../ui/buttons/button';\nimport {AddIcon} from '../../../icons/material/Add';\nimport {IconButton} from '../../../ui/buttons/icon-button';\nimport {CloseIcon} from '../../../icons/material/Close';\nimport {CreateProductPayload} from '../requests/use-create-product';\nimport {FormSwitch} from '../../../ui/forms/toggle/switch';\nimport {FormSelect} from '../../../ui/forms/select/select';\nimport {Item} from '../../../ui/forms/listbox/item';\nimport {FormFileSizeField} from '../../../ui/forms/input-field/file-size-field';\nimport {Link} from 'react-router-dom';\nimport {LinkStyle} from '../../../ui/buttons/external-link';\n\nexport function CrupdatePlanForm() {\n return (\n \n }\n className=\"mb-20\"\n required\n autoFocus\n />\n }\n className=\"mb-20\"\n inputElementType=\"textarea\"\n rows={4}\n />\n }\n className=\"mb-20\"\n >\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n }\n description={\n (\n \n {parts}\n \n ),\n }}\n message=\"Total storage space all user uploads are allowed to take up.\"\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\ninterface HeaderProps {\n children: ReactNode;\n}\nfunction Header({children}: HeaderProps) {\n return

{children}

;\n}\n\nfunction FeatureListForm() {\n const {fields, append, remove} = useFieldArray({\n name: 'feature_list',\n });\n return (\n
\n {fields.map((field, index) => {\n return (\n
\n \n {\n remove(index);\n }}\n >\n \n \n
\n );\n })}\n }\n size=\"xs\"\n onClick={() => {\n append({value: ''});\n }}\n >\n \n \n
\n );\n}\n\nfunction PricingListForm() {\n const {\n watch,\n formState: {errors},\n } = useFormContext();\n const {fields, append, remove} = useFieldArray<\n CreateProductPayload,\n 'prices',\n 'key'\n >({\n name: 'prices',\n keyName: 'key',\n });\n\n // if plan is marked as free, hide pricing form\n if (watch('free')) {\n return null;\n }\n\n return (\n \n
\n \n
\n {errors.prices?.message && (\n
{errors.prices.message}
\n )}\n \n {fields.map((field, index) => (\n }\n key={field.key}\n >\n {\n remove(index);\n }}\n />\n \n ))}\n \n }\n size=\"xs\"\n onClick={() => {\n append({\n currency: 'USD',\n amount: 1,\n interval_count: 1,\n interval: 'month',\n });\n }}\n >\n \n \n
\n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient, queryClient} from '../../../http/query-client';\nimport {BackendResponse} from '../../../http/backend-response/backend-response';\nimport {toast} from '../../../ui/toast/toast';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {message} from '../../../i18n/message';\nimport {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';\nimport {Product} from '../../../billing/product';\nimport {useNavigate} from '../../../utils/hooks/use-navigate';\nimport {CreateProductPayload} from './use-create-product';\nimport {UseFormReturn} from 'react-hook-form';\nimport {onFormQueryError} from '../../../errors/on-form-query-error';\n\ninterface Response extends BackendResponse {\n product: Product;\n}\n\nexport interface UpdateProductPayload extends CreateProductPayload {\n id: number;\n}\n\nconst Endpoint = (id: number) => `billing/products/${id}`;\n\nexport function useUpdateProduct(form: UseFormReturn) {\n const {trans} = useTrans();\n const navigate = useNavigate();\n return useMutation({\n mutationFn: (payload: UpdateProductPayload) => updateProduct(payload),\n onSuccess: response => {\n toast(trans(message('Plan updated')));\n queryClient.invalidateQueries({\n queryKey: [Endpoint(response.product.id)],\n });\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('billing/products'),\n });\n navigate('/admin/plans');\n },\n onError: err => onFormQueryError(err, form),\n });\n}\n\nfunction updateProduct({\n id,\n ...payload\n}: UpdateProductPayload): Promise {\n const backendPayload = {\n ...payload,\n feature_list: payload.feature_list.map(feature => feature.value),\n };\n return apiClient.put(Endpoint(id), backendPayload).then(r => r.data);\n}\n","import {FullPageLoader} from '../../../ui/progress/full-page-loader';\nimport {Trans} from '../../../i18n/trans';\nimport {useForm} from 'react-hook-form';\nimport {CrupdateResourceLayout} from '../../crupdate-resource-layout';\nimport {useProduct} from '../requests/use-product';\nimport {Product} from '../../../billing/product';\nimport {CrupdatePlanForm} from './crupdate-plan-form';\nimport {\n UpdateProductPayload,\n useUpdateProduct,\n} from '../requests/use-update-product';\n\nexport function EditPlanPage() {\n const query = useProduct();\n\n if (query.status !== 'success') {\n return ;\n }\n\n return ;\n}\n\ninterface PageContentProps {\n product: Product;\n}\nfunction PageContent({product}: PageContentProps) {\n const form = useForm({\n defaultValues: {\n ...product,\n feature_list: product.feature_list.map(f => ({value: f})),\n },\n });\n const updateProduct = useUpdateProduct(form);\n\n return (\n {\n updateProduct.mutate(values);\n }}\n title={\n \n }\n isLoading={updateProduct.isPending}\n >\n \n \n );\n}\n","import {Product} from '../../../billing/product';\nimport {useTrans} from '../../../i18n/use-trans';\nimport {useNavigate} from '../../../utils/hooks/use-navigate';\nimport {useMutation} from '@tanstack/react-query';\nimport {toast} from '../../../ui/toast/toast';\nimport {message} from '../../../i18n/message';\nimport {apiClient, queryClient} from '../../../http/query-client';\nimport {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';\nimport {Price} from '../../../billing/price';\nimport {onFormQueryError} from '../../../errors/on-form-query-error';\nimport {UseFormReturn} from 'react-hook-form';\n\nconst endpoint = 'billing/products';\n\nexport interface CreateProductPayload\n extends Omit, 'feature_list' | 'prices'> {\n feature_list: {value: string}[];\n prices: Omit[];\n}\n\nexport function useCreateProduct(form: UseFormReturn) {\n const {trans} = useTrans();\n const navigate = useNavigate();\n return useMutation({\n mutationFn: (payload: CreateProductPayload) => createProduct(payload),\n onSuccess: () => {\n toast(trans(message('Plan created')));\n queryClient.invalidateQueries({queryKey: [endpoint]});\n queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('billing/products'),\n });\n navigate('/admin/plans');\n },\n onError: err => onFormQueryError(err, form),\n });\n}\n\nfunction createProduct(payload: CreateProductPayload): Promise {\n const backendPayload = {\n ...payload,\n feature_list: payload.feature_list.map(feature => feature.value),\n };\n return apiClient.post(endpoint, backendPayload).then(r => r.data);\n}\n","import {useForm} from 'react-hook-form';\nimport {CrupdateResourceLayout} from '../../crupdate-resource-layout';\nimport {Trans} from '../../../i18n/trans';\nimport {CrupdatePlanForm} from './crupdate-plan-form';\nimport {\n CreateProductPayload,\n useCreateProduct,\n} from '../requests/use-create-product';\n\nexport function CreatePlanPage() {\n const form = useForm({\n defaultValues: {\n free: false,\n recommended: false,\n },\n });\n const createProduct = useCreateProduct(form);\n\n return (\n {\n createProduct.mutate(values);\n }}\n title={}\n isLoading={createProduct.isPending}\n >\n \n \n );\n}\n","import {SettingsPanel} from '../settings-panel';\nimport {SettingsSeparator} from '../settings-separator';\nimport {Trans} from '../../../i18n/trans';\nimport {FormSwitch} from '../../../ui/forms/toggle/switch';\nimport {useFieldArray, useFormContext} from 'react-hook-form';\nimport {AdminSettings} from '../admin-settings';\nimport React, {Fragment} from 'react';\nimport {FormSelect} from '../../../ui/forms/select/select';\nimport {Item} from '../../../ui/forms/listbox/item';\nimport {MenuItemForm} from '../../menus/menu-item-form';\nimport {Button} from '../../../ui/buttons/button';\nimport {AddIcon} from '../../../icons/material/Add';\nimport {DialogTrigger} from '../../../ui/overlays/dialog/dialog-trigger';\nimport {AddMenuItemDialog} from '../../appearance/sections/menus/add-menu-item-dialog';\nimport {Accordion, AccordionItem} from '../../../ui/accordion/accordion';\nimport {IconButton} from '../../../ui/buttons/icon-button';\nimport {CloseIcon} from '../../../icons/material/Close';\n\nexport function GdprSettings() {\n return (\n }\n description={\n \n }\n >\n \n \n \n \n );\n}\n\nfunction CookieNoticeSection() {\n const {watch} = useFormContext();\n const noticeEnabled = watch('client.cookie_notice.enable');\n\n return (\n
\n \n }\n >\n \n \n {noticeEnabled && (\n \n
\n
\n \n
\n \n
\n }\n className=\"mb-20\"\n >\n \n \n \n \n \n \n \n
\n )}\n
\n );\n}\n\nfunction RegistrationPoliciesSection() {\n const {fields, append, remove} = useFieldArray<\n AdminSettings,\n 'client.registration.policies'\n >({\n name: 'client.registration.policies',\n });\n\n return (\n \n
\n \n
\n
\n \n
\n \n {fields.map((field, index) => (\n {\n remove(index);\n }}\n >\n \n \n }\n >\n \n \n ))}\n \n {\n if (value) {\n append(value);\n }\n }}\n >\n }\n size=\"xs\"\n >\n \n \n } />\n \n
\n );\n}\n","import {createSvgIcon} from '@common/icons/create-svg-icon';\n\nexport const InfoDialogTriggerIcon = createSvgIcon(\n ,\n 'InfoDialogTrigger'\n);\n","import {IconButton} from '@common/ui/buttons/icon-button';\nimport {InfoDialogTriggerIcon} from '@common/ui/overlays/dialog/info-dialog-trigger/info-dialog-trigger-icon';\nimport {Dialog, DialogSize} from '@common/ui/overlays/dialog/dialog';\nimport {DialogHeader} from '@common/ui/overlays/dialog/dialog-header';\nimport {DialogBody} from '@common/ui/overlays/dialog/dialog-body';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport React, {ReactNode} from 'react';\nimport clsx from 'clsx';\n\ninterface Props {\n title?: ReactNode;\n body: ReactNode;\n dialogSize?: DialogSize;\n className?: string;\n}\nexport function InfoDialogTrigger({\n title,\n body,\n dialogSize = 'sm',\n className,\n}: Props) {\n return (\n \n \n \n \n \n {title && (\n \n {title}\n \n )}\n {body}\n \n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const HomeIcon = createSvgIcon(\n \n, 'HomeOutlined');\n","import {ColumnConfig} from '@common/datatable/column-config';\nimport {Trans} from '@common/i18n/trans';\nimport {FormattedDate} from '@common/i18n/formatted-date';\nimport {Link} from 'react-router-dom';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {EditIcon} from '@common/icons/material/Edit';\nimport React from 'react';\nimport {Channel} from '@common/channels/channel';\nimport {Chip} from '@common/ui/forms/input-field/chip-field/chip';\nimport {Tooltip} from '@common/ui/tooltip/tooltip';\nimport {useSettings} from '@common/core/settings/use-settings';\nimport {HomeIcon} from '@common/icons/material/Home';\n\nexport const ChannelsDatatableColumns: ColumnConfig[] = [\n {\n key: 'name',\n allowsSorting: true,\n width: 'flex-3',\n visibleInMode: 'all',\n header: () => ,\n body: channel => {\n return (\n
\n
\n \n
\n {channel.config.adminDescription && (\n

\n {channel.config.adminDescription}\n

\n )}\n
\n );\n },\n },\n {\n key: 'content',\n allowsSorting: false,\n header: () => ,\n body: channel => ,\n },\n {\n key: 'content_type',\n allowsSorting: false,\n header: () => ,\n body: channel => (\n \n {channel.config.contentModel ? (\n \n ) : undefined}\n \n ),\n },\n {\n key: 'internal',\n allowsSorting: true,\n maxWidth: 'max-w-100',\n hideHeader: true,\n header: () => ,\n body: channel => ,\n },\n {\n key: 'updated_at',\n allowsSorting: true,\n maxWidth: 'max-w-100',\n header: () => ,\n body: channel =>\n channel.updated_at ? : '',\n },\n {\n key: 'actions',\n header: () => ,\n hideHeader: true,\n visibleInMode: 'all',\n align: 'end',\n width: 'w-42 flex-shrink-0',\n body: channel => (\n \n \n \n \n \n ),\n },\n];\n\ninterface ContentTypeProps {\n channel: Channel;\n}\nfunction ContentType({channel}: ContentTypeProps) {\n switch (channel.config.contentType) {\n case 'listAll':\n return ;\n case 'manual':\n return ;\n case 'autoUpdate':\n return ;\n }\n}\n\ninterface ChannelNameProps {\n channel: Channel;\n}\nfunction ChannelName({channel}: ChannelNameProps) {\n // link will not work without specific genre name in channel url\n if (\n channel.config.restriction &&\n channel.config.restrictionModelId === 'urlParam'\n ) {\n return channel.name;\n }\n return (\n \n {channel.name}\n \n );\n}\n\nfunction InternalColumn({channel}: ChannelNameProps) {\n const {homepage} = useSettings();\n const internalLabel = channel.internal ? (\n \n }\n >\n
\n \n \n \n
\n \n ) : (\n ''\n );\n\n const isHomepage =\n homepage?.type === 'channels' && `${homepage.value}` === `${channel.id}`;\n\n return (\n
\n {internalLabel}\n {isHomepage ? : null}\n
\n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\ninterface Payload {\n preset: string;\n}\n\nexport function useApplyChannelPreset() {\n const {trans} = useTrans();\n return useMutation({\n mutationFn: (payload: Payload) => resetChannels(payload),\n onSuccess: async () => {\n await queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('channel'),\n });\n toast(trans(message('Channel preset applied')));\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction resetChannels(payload: Payload) {\n return apiClient\n .post('channel/apply-preset', payload)\n .then(r => r.data);\n}\n","import {LearnMoreLink} from '@common/admin/settings/learn-more-link';\nimport {useContext} from 'react';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\n\ninterface Props {\n className?: string;\n hash?: string;\n}\nexport function ChannelsDocsLink({className, hash}: Props) {\n const {admin} = useContext(SiteConfigContext);\n if (!admin?.channelsDocsLink) return null;\n const link = hash\n ? `${admin.channelsDocsLink}#${hash}`\n : admin.channelsDocsLink;\n return ;\n}\n","import React, {Fragment} from 'react';\nimport {Trans} from '@common/i18n/trans';\nimport {DataTableEmptyStateMessage} from '@common/datatable/page/data-table-emty-state-message';\nimport playlist from './playlist.svg';\nimport {DataTableAddItemButton} from '@common/datatable/data-table-add-item-button';\nimport {InfoDialogTrigger} from '@common/ui/overlays/dialog/info-dialog-trigger/info-dialog-trigger';\nimport {Link} from 'react-router-dom';\nimport {ChannelsDatatableColumns} from '@common/admin/channels/channels-datatable-columns';\nimport {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';\nimport {useApplyChannelPreset} from '@common/admin/channels/requests/use-apply-channel-preset';\nimport {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';\nimport {DataTablePage} from '@common/datatable/page/data-table-page';\nimport {DeleteSelectedItemsAction} from '@common/datatable/page/delete-selected-items-action';\nimport {useDataTable} from '@common/datatable/page/data-table-context';\nimport {Channel} from '@common/channels/channel';\nimport {Menu, MenuTrigger} from '@common/ui/navigation/menu/menu-trigger';\nimport {Button} from '@common/ui/buttons/button';\nimport {Item} from '@common/ui/forms/listbox/item';\nimport {KeyboardArrowDownIcon} from '@common/icons/material/KeyboardArrowDown';\nimport {openDialog} from '@common/ui/overlays/store/dialog-store';\nimport {ChannelsDocsLink} from '@common/admin/channels/channels-docs-link';\n\ninterface ChannelPresetConfig {\n preset: string;\n name: string;\n description: string;\n}\n\nexport function ChannelsDatatablePage() {\n return (\n }\n headerContent={}\n headerItemsAlign=\"items-center\"\n queryParams={{type: 'channel'}}\n columns={ChannelsDatatableColumns}\n actions={}\n selectedActions={}\n cellHeight=\"h-52\"\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n );\n}\n\nfunction InfoTrigger() {\n return (\n \n \n \n \n }\n />\n );\n}\n\nfunction Actions() {\n const {query} = useDataTable();\n return (\n \n openDialog(ApplyPresetDialog, {preset})}\n >\n }\n disabled={!query.data?.presets.length}\n >\n \n \n \n {query.data?.presets.map(preset => (\n }\n >\n \n \n ))}\n \n \n \n \n \n \n );\n}\n\ninterface ApplyPresetDialogProps {\n preset: string;\n}\nfunction ApplyPresetDialog({preset}: ApplyPresetDialogProps) {\n const {close} = useDialogContext();\n const resetChannels = useApplyChannelPreset();\n return (\n {\n resetChannels.mutate({preset}, {onSuccess: () => close()});\n }}\n isDanger\n title={}\n body={\n \n }\n confirm={}\n />\n );\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {useNavigate} from '@common/utils/hooks/use-navigate';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {UseFormReturn} from 'react-hook-form';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {Channel} from '@common/channels/channel';\nimport {CreateChannelPayload} from '@common/admin/channels/requests/use-create-channel';\n\ninterface Response extends BackendResponse {\n channel: Channel;\n}\n\nexport interface UpdateChannelPayload extends CreateChannelPayload {\n id: number;\n}\n\nconst Endpoint = (id: number) => `channel/${id}`;\n\nexport function useUpdateChannel(form: UseFormReturn) {\n const {trans} = useTrans();\n const navigate = useNavigate();\n return useMutation({\n mutationFn: (payload: UpdateChannelPayload) => updateChannel(payload),\n onSuccess: async () => {\n await queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('channel'),\n });\n toast(trans(message('Channel updated')));\n navigate('/admin/channels');\n },\n onError: err => onFormQueryError(err, form),\n });\n}\n\nfunction updateChannel({\n id,\n ...payload\n}: UpdateChannelPayload): Promise {\n return apiClient.put(Endpoint(id), payload).then(r => r.data);\n}\n","import {useForm} from 'react-hook-form';\nimport React, {ReactNode} from 'react';\nimport {CrupdateResourceLayout} from '@common/admin/crupdate-resource-layout';\nimport {Trans} from '@common/i18n/trans';\nimport {PageStatus} from '@common/http/page-status';\nimport {useChannel} from '@common/channels/requests/use-channel';\nimport {Channel} from '@common/channels/channel';\nimport {\n UpdateChannelPayload,\n useUpdateChannel,\n} from '@common/admin/channels/requests/use-update-channel';\n\ninterface Props {\n children: ReactNode;\n}\nexport function EditChannelPageLayout({children}: Props) {\n const query = useChannel(undefined, 'editChannelPage');\n if (query.data) {\n return {children};\n }\n return ;\n}\n\ninterface PageContentProps {\n channel: Channel;\n children: ReactNode;\n}\nfunction PageContent({channel, children}: PageContentProps) {\n const form = useForm({\n // @ts-ignore\n defaultValues: {\n ...channel,\n },\n });\n const updateChannel = useUpdateChannel(form);\n\n return (\n {\n updateChannel.mutate(values);\n }}\n title={\n \n }\n isLoading={updateChannel.isPending}\n >\n {children}\n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const DescriptionIcon = createSvgIcon(\n \n, 'DescriptionOutlined');\n","import React, {Fragment, useEffect, useState} from 'react';\nimport clsx from 'clsx';\nimport {RefCallBack} from 'react-hook-form';\nimport {Button} from './buttons/button';\nimport {LinkIcon} from '../icons/material/Link';\nimport {TextField} from './forms/input-field/text-field/text-field';\nimport {Trans} from '../i18n/trans';\nimport {useSettings} from '../core/settings/use-settings';\nimport {slugifyString} from '@common/utils/string/slugify-string';\n\nexport interface SlugEditorProps {\n prefix?: string;\n suffix?: string;\n host?: string;\n value?: string | null;\n placeholder?: string;\n onChange?: (value: string) => void;\n className?: string;\n inputRef?: RefCallBack;\n onInputBlur?: () => void;\n showLinkIcon?: boolean;\n pattern?: string;\n minLength?: number;\n maxLength?: number;\n hideButton?: boolean;\n}\nexport function SlugEditor({\n host,\n value: initialValue = '',\n placeholder,\n onChange,\n className,\n inputRef,\n onInputBlur,\n showLinkIcon = true,\n pattern,\n minLength,\n maxLength,\n hideButton,\n ...props\n}: SlugEditorProps) {\n const {base_url} = useSettings();\n const prefix = props.prefix ? `/${props.prefix}` : '';\n const suffix = props.suffix ? `/${props.suffix}` : '';\n const [isEditing, setIsEditing] = useState(false);\n const [value, setValue] = useState(initialValue);\n host = host || base_url;\n\n useEffect(() => {\n setValue(initialValue);\n }, [initialValue]);\n\n const handleSubmit = () => {\n if (!isEditing) {\n setIsEditing(true);\n } else {\n setIsEditing(false);\n if (value) {\n onChange?.(value);\n }\n }\n };\n\n let preview: string = '';\n if (value) {\n preview = value;\n } else if (placeholder) {\n preview = slugifyString(placeholder);\n }\n\n return (\n // can't use
here as component might be used inside another form\n
\n {showLinkIcon && }\n
\n {host}\n {prefix}\n {!isEditing && preview && (\n \n /\n {preview}\n \n )}\n {!isEditing ? suffix : null}\n
\n {isEditing && (\n {\n if (e.key === 'Enter') {\n handleSubmit();\n }\n }}\n ref={inputRef}\n aria-label=\"slug\"\n autoFocus\n className=\"mr-14\"\n size=\"2xs\"\n value={value as string}\n onBlur={onInputBlur}\n onChange={e => {\n setValue(e.target.value);\n }}\n />\n )}\n {!hideButton && (\n {\n handleSubmit();\n }}\n >\n {isEditing ? : }\n \n )}\n
\n );\n}\n","import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {Trans} from '@common/i18n/trans';\nimport React, {Fragment} from 'react';\nimport {useFormContext} from 'react-hook-form';\nimport {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';\nimport {SlugEditor} from '@common/ui/slug-editor';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {message} from '@common/i18n/message';\nimport clsx from 'clsx';\n\ninterface Props {\n className?: string;\n autoFocus?: boolean;\n}\nexport function ChannelNameField({className, autoFocus}: Props) {\n return (\n \n }\n required\n autoFocus={autoFocus}\n className={clsx('mb-10', className)}\n />\n \n \n );\n}\n\nfunction FormSlugField() {\n const {watch, setValue} = useFormContext();\n const value = watch('slug');\n const name = watch('name');\n const disableSlugEditing = watch('config.lockSlug');\n const restriction = watch('config.restriction');\n const restrictionId = watch('config.restrictionModelId');\n const {trans} = useTrans();\n return (\n {\n setValue('slug', newSlug);\n }}\n />\n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {FormSelect, Option} from '@common/ui/forms/select/select';\nimport {Trans} from '@common/i18n/trans';\nimport {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';\nimport {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';\n\ninterface Props {\n config: ChannelContentConfig;\n className?: string;\n}\nexport function ContentTypeField({config, className}: Props) {\n const {setValue} = useFormContext();\n return (\n }\n onSelectionChange={newValue => {\n // if content type is \"auto update\" select first model that\n // can be auto updated, otherwise select first available model\n let model = Object.entries(config.models)[0];\n if (newValue === 'autoUpdate') {\n const newModel = Object.entries(config.models).find(\n ([, modelConfig]) => modelConfig.autoUpdateMethods?.length,\n );\n if (newModel) {\n model = newModel;\n }\n }\n const [modelName, modelConfig] = model;\n\n setValue('config.contentModel', modelName);\n setValue('config.restrictionModelId', undefined);\n setValue(\n 'config.autoUpdateMethod',\n newValue === 'autoUpdate' ? modelConfig.autoUpdateMethods?.[0] : '',\n );\n setValue('config.contentOrder', modelConfig.sortMethods[0]);\n (setValue as any)('config.restriction', null);\n }}\n >\n \n \n \n \n );\n}\n","import {useFormContext} from 'react-hook-form';\nimport {FormSelect, Option} from '@common/ui/forms/select/select';\nimport {Trans} from '@common/i18n/trans';\nimport {InfoDialogTrigger} from '@common/ui/overlays/dialog/info-dialog-trigger/info-dialog-trigger';\nimport {Fragment, ReactNode} from 'react';\nimport {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';\nimport {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport clsx from 'clsx';\nimport {ChannelsDocsLink} from '@common/admin/channels/channels-docs-link';\n\ninterface Props {\n children?: ReactNode;\n config: ChannelContentConfig;\n className?: string;\n}\nexport function ContentAutoUpdateField({children, config, className}: Props) {\n const {watch, setValue} = useFormContext();\n const modelConfig = config.models[watch('config.contentModel')];\n const selectedMethodConfig =\n config.autoUpdateMethods[watch('config.autoUpdateMethod')!];\n\n if (\n watch('config.contentType') !== 'autoUpdate' ||\n !modelConfig.autoUpdateMethods?.length\n ) {\n return null;\n }\n\n return (\n
\n {\n if (config.autoUpdateMethods[value].provider) {\n setValue(\n 'config.autoUpdateProvider',\n config.autoUpdateMethods[value].provider,\n );\n }\n }}\n label={\n \n \n \n
\n \n
\n \n
\n }\n />\n \n }\n >\n {modelConfig.autoUpdateMethods.map(method => (\n \n ))}\n \n {selectedMethodConfig?.value ? (\n }\n type={selectedMethodConfig?.value.inputType}\n />\n ) : null}\n {children}\n
\n );\n}\n","import {useSettings} from '@common/core/settings/use-settings';\nimport {useFormContext} from 'react-hook-form';\nimport {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';\nimport {channelContentConfig} from '@app/admin/channels/channel-content-config';\nimport {ContentAutoUpdateField} from '@common/admin/channels/channel-editor/controls/content-auto-update-field';\nimport {FormSelect, Option} from '@common/ui/forms/select/select';\nimport {Trans} from '@common/i18n/trans';\nimport React from 'react';\n\ninterface Props {\n className?: string;\n}\nexport function ChannelAutoUpdateField({className}: Props) {\n const {tmdb_is_setup} = useSettings();\n const {watch} = useFormContext();\n const methodConfig =\n channelContentConfig.autoUpdateMethods[watch('config.autoUpdateMethod')!];\n return (\n \n {!methodConfig?.provider && tmdb_is_setup && (\n }\n required\n >\n \n \n \n )}\n \n );\n}\n","export const KEYWORD_MODEL = 'keyword';\n\nexport interface Keyword {\n id: number;\n name: string;\n display_name: string;\n updated_at: string;\n created_at: string;\n model_type: typeof KEYWORD_MODEL;\n}\n","import {Trans} from '@common/i18n/trans';\nimport {Item} from '@common/ui/forms/listbox/item';\nimport {FormSelect} from '@common/ui/forms/select/select';\nimport React, {Fragment, useState} from 'react';\nimport {GENRE_MODEL} from '@app/titles/models/genre';\nimport {KEYWORD_MODEL} from '@app/titles/models/keyword';\nimport {PRODUCTION_COUNTRY_MODEL} from '@app/titles/models/production-country';\nimport {useValueLists} from '@common/http/value-lists';\nimport {message} from '@common/i18n/message';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {useFormContext} from 'react-hook-form';\nimport {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';\nimport {MOVIE_MODEL, SERIES_MODEL, TITLE_MODEL} from '@app/titles/models/title';\nimport {InfoDialogTrigger} from '@common/ui/overlays/dialog/info-dialog-trigger/info-dialog-trigger';\nimport clsx from 'clsx';\nimport {ChannelsDocsLink} from '@common/admin/channels/channels-docs-link';\n\nconst supportedModels = [TITLE_MODEL, MOVIE_MODEL, SERIES_MODEL];\n\nconst restrictions = {\n [GENRE_MODEL]: message('Genre'),\n [KEYWORD_MODEL]: message('Keyword'),\n [PRODUCTION_COUNTRY_MODEL]: message('Production country'),\n};\n\ninterface Props {\n className?: string;\n}\nexport function ChannelRestrictionField({className}: Props) {\n const {setValue} = useFormContext();\n const {watch} = useFormContext();\n\n if (!supportedModels.includes(watch('config.contentModel'))) {\n return null;\n }\n\n return (\n
\n \n \n \n \n }\n onSelectionChange={() => {\n setValue('config.restrictionModelId', 'urlParam');\n }}\n >\n \n \n \n {Object.entries(restrictions).map(([value, label]) => (\n \n \n \n ))}\n \n \n
\n );\n}\n\nfunction RestrictionModelField() {\n const {trans} = useTrans();\n const [searchValue, setSearchValue] = useState('');\n const {watch} = useFormContext();\n const {data} = useValueLists(['genres', 'productionCountries'], {\n type: watch('config.autoUpdateProvider'),\n });\n\n const selectedRestriction = watch(\n 'config.restriction',\n ) as keyof typeof restrictions;\n const selectedKeywordId = watch('config.restrictionModelId');\n const keywordQuery = useValueLists(['keywords'], {\n searchQuery: searchValue,\n selectedValue: selectedKeywordId,\n type: watch('config.autoUpdateProvider'),\n });\n\n if (!selectedRestriction) return null;\n\n const options = {\n [GENRE_MODEL]: data?.genres,\n [KEYWORD_MODEL]: keywordQuery.data?.keywords,\n [PRODUCTION_COUNTRY_MODEL]: data?.productionCountries,\n };\n const restrictionLabel = restrictions[selectedRestriction];\n\n // allow setting keyword to custom value, because there are too many keywords\n // to put into autocomplete, ideally it would use async search from backend though\n\n return (\n \n }\n >\n \n \n \n {options[selectedRestriction]?.map(option => (\n \n \n \n ))}\n \n );\n}\n\nfunction InfoTrigger() {\n return (\n \n \n \n \n }\n />\n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const DashboardIcon = createSvgIcon(\n \n, 'DashboardOutlined');\n","import {useFormContext} from 'react-hook-form';\nimport {FormSelect, Option} from '@common/ui/forms/select/select';\nimport {Trans} from '@common/i18n/trans';\nimport {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';\nimport {ReactNode} from 'react';\nimport {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';\nimport clsx from 'clsx';\n\ninterface Props {\n config: ChannelContentConfig;\n className?: string;\n}\nexport function ContentLayoutFields({config, className}: Props) {\n return (\n
\n }\n />\n }\n />\n
\n );\n}\n\ninterface LayoutFieldProps extends Props {\n name: string;\n label: ReactNode;\n}\nfunction LayoutField({config, name, label}: LayoutFieldProps) {\n const {watch} = useFormContext();\n const contentModel = watch('config.contentModel');\n const modelConfig = config.models[contentModel];\n\n if (!modelConfig.layoutMethods?.length) {\n return null;\n }\n\n return (\n \n {modelConfig.layoutMethods.map(method => {\n const label = config.layoutMethods[method].label;\n return (\n \n );\n })}\n \n );\n}\n","import {FormSelect, Option} from '@common/ui/forms/select/select';\nimport {Trans} from '@common/i18n/trans';\nimport {ChannelContentConfig} from '@common/admin/channels/channel-editor/channel-content-config';\n\ninterface Props {\n config: ChannelContentConfig;\n className?: string;\n}\nexport function ChannelPaginationTypeField({className}: Props) {\n return (\n }\n >\n \n \n \n \n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const PublicIcon = createSvgIcon(\n \n, 'PublicOutlined');\n","import React, {Fragment} from 'react';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {Trans} from '@common/i18n/trans';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {message} from '@common/i18n/message';\n\nexport function ChannelSeoFields() {\n const {trans} = useTrans();\n return (\n \n }\n className=\"mb-24\"\n placeholder={trans(message('Optional'))}\n />\n }\n inputElementType=\"textarea\"\n rows={6}\n placeholder={trans(message('Optional'))}\n />\n \n );\n}\n","import {EditChannelPageLayout} from '@common/admin/channels/channel-editor/edit-channel-page-layout';\nimport React, {Fragment} from 'react';\nimport {Accordion, AccordionItem} from '@common/ui/accordion/accordion';\nimport {Trans} from '@common/i18n/trans';\nimport {DescriptionIcon} from '@common/icons/material/Description';\nimport {ChannelNameField} from '@common/admin/channels/channel-editor/controls/channel-name-field';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {InfoDialogTrigger} from '@common/ui/overlays/dialog/info-dialog-trigger/info-dialog-trigger';\nimport {SettingsIcon} from '@common/icons/material/Settings';\nimport {ContentTypeField} from '@common/admin/channels/channel-editor/controls/content-type-field';\nimport {channelContentConfig} from '@app/admin/channels/channel-content-config';\nimport {ChannelAutoUpdateField} from '@app/admin/channels/channel-auto-update-field';\nimport {ContentModelField} from '@common/admin/channels/channel-editor/controls/content-model-field';\nimport {ChannelRestrictionField} from '@app/admin/channels/channel-restriction-field';\nimport {ContentOrderField} from '@common/admin/channels/channel-editor/controls/content-order-field';\nimport {DashboardIcon} from '@common/icons/material/Dashboard';\nimport {ContentLayoutFields} from '@common/admin/channels/channel-editor/controls/content-layout-fields';\nimport {ChannelPaginationTypeField} from '@common/admin/channels/channel-editor/controls/channel-pagination-type-field';\nimport {PublicIcon} from '@common/icons/material/Public';\nimport {ChannelSeoFields} from '@app/admin/channels/channel-seo-fields';\nimport {ChannelContentEditor} from '@common/admin/channels/channel-editor/channel-content-editor';\nimport {\n ChannelContentSearchField,\n ChannelContentSearchFieldProps,\n} from '@common/admin/channels/channel-editor/channel-content-search-field';\nimport {ChannelContentItemImage} from '@app/admin/channels/channel-content-item-image';\n\nexport function EditChannelPage() {\n return (\n \n \n \n }\n startIcon={}\n >\n \n \n }\n >\n \n \n }\n inputElementType=\"textarea\"\n rows={1}\n className=\"mt-24\"\n />\n \n \n \n }\n />\n \n }\n inputElementType=\"textarea\"\n rows={1}\n className=\"mt-24\"\n />\n \n }\n startIcon={}\n >\n \n \n \n \n \n \n }\n startIcon={}\n >\n \n \n \n }\n startIcon={}\n >\n \n \n \n } />\n \n \n );\n}\n\nfunction SearchField(props: ChannelContentSearchFieldProps) {\n return (\n }\n />\n );\n}\n","import {useMutation, useQueryClient} from '@tanstack/react-query';\nimport {UseFormReturn} from 'react-hook-form';\nimport {apiClient} from '@common/http/query-client';\nimport {toast} from '@common/ui/toast/toast';\nimport {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';\nimport {useTrans} from '@common/i18n/use-trans';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {message} from '@common/i18n/message';\nimport {useNavigate} from '@common/utils/hooks/use-navigate';\nimport {PaginationResponse} from '@common/http/backend-response/pagination-response';\nimport {NormalizedModel} from '@common/datatable/filters/normalized-model';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {Channel} from '@common/channels/channel';\n\nconst endpoint = 'channel';\n\ninterface Response extends BackendResponse {\n channel: Channel;\n}\n\nexport interface CreateChannelPayload\n extends Omit {\n content: PaginationResponse;\n}\n\nexport function useCreateChannel(form: UseFormReturn) {\n const {trans} = useTrans();\n const navigate = useNavigate();\n const queryClient = useQueryClient();\n return useMutation({\n mutationFn: (payload: CreateChannelPayload) => createChannel(payload),\n onSuccess: async response => {\n await queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey(endpoint),\n });\n toast(trans(message('Channel created')));\n navigate(`/admin/channels/${response.channel.id}/edit`, {\n replace: true,\n });\n },\n onError: err => onFormQueryError(err, form),\n });\n}\n\nfunction createChannel(payload: CreateChannelPayload) {\n return apiClient.post(endpoint, payload).then(r => r.data);\n}\n","import {useForm} from 'react-hook-form';\nimport React, {ReactNode} from 'react';\nimport {CrupdateResourceLayout} from '@common/admin/crupdate-resource-layout';\nimport {Trans} from '@common/i18n/trans';\nimport {EMPTY_PAGINATION_RESPONSE} from '@common/http/backend-response/pagination-response';\nimport {UpdateChannelPayload} from '@common/admin/channels/requests/use-update-channel';\nimport {useCreateChannel} from '@common/admin/channels/requests/use-create-channel';\n\ninterface Props {\n defaultValues?: Partial;\n children: ReactNode;\n}\nexport function CreateChannelPageLayout({defaultValues, children}: Props) {\n const form = useForm({\n defaultValues: {\n content: EMPTY_PAGINATION_RESPONSE.pagination,\n config: {\n contentType: 'listAll',\n contentOrder: 'created_at:desc',\n nestedLayout: 'carousel',\n ...defaultValues,\n },\n },\n });\n const createChannel = useCreateChannel(form);\n\n return (\n {\n createChannel.mutate(values);\n }}\n title={}\n isLoading={createChannel.isPending}\n >\n {children}\n \n );\n}\n","import React, {ReactElement} from 'react';\nimport {CreateChannelPageLayout} from '@common/admin/channels/channel-editor/create-channel-page-layout';\nimport {MOVIE_MODEL} from '@app/titles/models/title';\nimport {Trans} from '@common/i18n/trans';\nimport {ChannelNameField} from '@common/admin/channels/channel-editor/controls/channel-name-field';\nimport {FormSwitch} from '@common/ui/forms/toggle/switch';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {ContentTypeField} from '@common/admin/channels/channel-editor/controls/content-type-field';\nimport {channelContentConfig} from '@app/admin/channels/channel-content-config';\nimport {ContentModelField} from '@common/admin/channels/channel-editor/controls/content-model-field';\nimport {ChannelRestrictionField} from '@app/admin/channels/channel-restriction-field';\nimport {ContentOrderField} from '@common/admin/channels/channel-editor/controls/content-order-field';\nimport {ContentLayoutFields} from '@common/admin/channels/channel-editor/controls/content-layout-fields';\nimport {ChannelPaginationTypeField} from '@common/admin/channels/channel-editor/controls/channel-pagination-type-field';\nimport {ChannelAutoUpdateField} from '@app/admin/channels/channel-auto-update-field';\nimport {ChannelSeoFields} from '@app/admin/channels/channel-seo-fields';\nimport clsx from 'clsx';\nimport {Tabs} from '@common/ui/tabs/tabs';\nimport {TabList} from '@common/ui/tabs/tab-list';\nimport {Tab} from '@common/ui/tabs/tab';\nimport {TabPanel, TabPanels} from '@common/ui/tabs/tab-panels';\n\nexport function CreateChannelPage() {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n }\n >\n \n \n }\n inputElementType=\"textarea\"\n rows={2}\n className=\"my-24\"\n />\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n}\n\ninterface TitleProps {\n children: ReactElement;\n className?: string;\n}\nfunction Title({children, className}: TitleProps) {\n return (\n

\n {children}\n

\n );\n}\n","import {BackendFilter} from '@common/datatable/filters/backend-filter';\nimport {message} from '@common/i18n/message';\nimport {\n createdAtFilter,\n updatedAtFilter,\n} from '@common/datatable/filters/timestamp-filters';\n\nexport const NewsDatatableFilters: BackendFilter[] = [\n createdAtFilter({\n description: message('Date article was created'),\n }),\n updatedAtFilter({\n description: message('Date article was last updated'),\n }),\n];\n","export default \"__VITE_ASSET__421a551f__\"","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\nimport {DatatableDataQueryKey} from '@common/datatable/requests/paginated-resources';\n\ninterface Response extends BackendResponse {}\n\ninterface Payload {\n articleId: number;\n}\n\nexport function useDeleteNewsArticle() {\n return useMutation({\n mutationFn: (payload: Payload) => deleteArticle(payload),\n onError: err => showHttpErrorToast(err),\n onSuccess: async () => {\n await queryClient.invalidateQueries({\n queryKey: DatatableDataQueryKey('news'),\n });\n toast(message('Article deleted'));\n },\n });\n}\n\nfunction deleteArticle(payload: Payload): Promise {\n return apiClient.delete(`news/${payload.articleId}`).then(r => r.data);\n}\n","import {ColumnConfig} from '@common/datatable/column-config';\nimport {NewsArticle} from '@app/titles/models/news-article';\nimport {Trans} from '@common/i18n/trans';\nimport {FormattedDate} from '@common/i18n/formatted-date';\nimport {Link} from 'react-router-dom';\nimport {Tooltip} from '@common/ui/tooltip/tooltip';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {EditIcon} from '@common/icons/material/Edit';\nimport {useContext} from 'react';\nimport {TableContext} from '@common/ui/tables/table-context';\nimport clsx from 'clsx';\nimport {useDeleteNewsArticle} from '@app/admin/news/requests/use-delete-news-article';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {DeleteIcon} from '@common/icons/material/Delete';\nimport {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';\nimport {useDialogContext} from '@common/ui/overlays/dialog/dialog-context';\nimport {NewsArticleLink} from '@app/news/news-article-link';\nimport {NewsArticleImage} from '@app/news/news-article-image';\n\nexport const newsDatatableColumns: ColumnConfig[] = [\n {\n key: 'name',\n width: 'flex-3 min-w-200',\n visibleInMode: 'all',\n header: () => ,\n body: article => ,\n },\n {\n key: 'updatedAt',\n allowsSorting: true,\n width: 'w-96',\n header: () => ,\n body: article => (\n \n ),\n },\n {\n key: 'actions',\n header: () => ,\n width: 'w-84 flex-shrink-0',\n hideHeader: true,\n align: 'end',\n visibleInMode: 'all',\n body: article => (\n
\n \n }>\n \n \n \n \n \n \n }>\n \n \n \n \n \n \n
\n ),\n },\n];\n\ninterface ArticleColumnProps {\n article: NewsArticle;\n}\nfunction ArticleColumn({article}: ArticleColumnProps) {\n const {isCollapsedMode} = useContext(TableContext);\n return (\n
\n \n
\n \n \n
\n {!isCollapsedMode && (\n

\n {article.body}\n

\n )}\n
\n
\n );\n}\n\ninterface DeleteArticleDialogProps {\n article: NewsArticle;\n}\nexport function DeleteArticleDialog({article}: DeleteArticleDialogProps) {\n const deleteArticle = useDeleteNewsArticle();\n const {close} = useDialogContext();\n return (\n }\n body={}\n confirm={}\n onConfirm={() => {\n deleteArticle.mutate(\n {articleId: article.id},\n {onSuccess: () => close()},\n );\n }}\n />\n );\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const PublishIcon = createSvgIcon(\n \n, 'PublishOutlined');\n","import {useMutation} from '@tanstack/react-query';\nimport {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {toast} from '@common/ui/toast/toast';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {message} from '@common/i18n/message';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {}\n\nexport function useImportNewsArticles() {\n return useMutation({\n mutationFn: () => importArticles(),\n onSuccess: async () => {\n await queryClient.invalidateQueries({queryKey: ['news']});\n toast(message('Imported news articles'));\n },\n onError: r => showHttpErrorToast(r),\n });\n}\n\nfunction importArticles(): Promise {\n return apiClient.post(`news/import-from-remote-provider`).then(r => r.data);\n}\n","import {Fragment} from 'react';\nimport {Trans} from '@common/i18n/trans';\nimport {Link} from 'react-router-dom';\nimport {DataTablePage} from '@common/datatable/page/data-table-page';\nimport {NewsDatatableFilters} from '@app/admin/news/news-datatable-filters';\nimport {DataTableAddItemButton} from '@common/datatable/data-table-add-item-button';\nimport {DataTableEmptyStateMessage} from '@common/datatable/page/data-table-emty-state-message';\nimport {DeleteSelectedItemsAction} from '@common/datatable/page/delete-selected-items-action';\nimport onlineArticlesImg from '@app/admin/news/online-articles.svg';\nimport {newsDatatableColumns} from '@app/admin/news/news-datatable-columns';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {PublishIcon} from '@common/icons/material/Publish';\nimport {useImportNewsArticles} from '@app/admin/news/requests/use-import-news-articles';\nimport {Tooltip} from '@common/ui/tooltip/tooltip';\n\nexport function NewsDatatablePage() {\n return (\n }\n filters={NewsDatatableFilters}\n columns={newsDatatableColumns}\n queryParams={{\n stripHtml: 'true',\n truncateBody: 200,\n }}\n actions={}\n selectedActions={}\n enableSelection={false}\n cellHeight=\"h-80\"\n emptyStateMessage={\n }\n filteringTitle={}\n />\n }\n />\n );\n}\n\nfunction Actions() {\n const importArticles = useImportNewsArticles();\n return (\n \n }>\n importArticles.mutate()}\n disabled={importArticles.isPending}\n >\n \n \n \n \n \n \n \n );\n}\n","import {useDeleteComments} from '@common/comments/requests/use-delete-comments';\nimport {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {queryClient} from '@common/http/query-client';\nimport {Button} from '@common/ui/buttons/button';\nimport {Trans} from '@common/i18n/trans';\nimport {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';\nimport React from 'react';\nimport {ButtonVariant} from '@common/ui/buttons/get-shared-button-style';\nimport {ButtonSize} from '@common/ui/buttons/button-size';\n\ninterface DeleteCommentsButtonProps {\n commentIds: number[];\n variant?: ButtonVariant;\n size?: ButtonSize;\n}\nexport function DeleteCommentsButton({\n commentIds,\n variant = 'outline',\n size = 'xs',\n}: DeleteCommentsButtonProps) {\n const deleteComments = useDeleteComments();\n return (\n {\n if (isConfirmed) {\n deleteComments.mutate(\n {commentIds},\n {\n onSuccess: () => {\n queryClient.invalidateQueries({queryKey: ['comment']});\n },\n },\n );\n }\n }}\n >\n \n \n \n \n }\n body={\n commentIds.length > 1 ? (\n \n ) : (\n \n )\n }\n confirm={}\n />\n \n );\n}\n","import {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {useMutation} from '@tanstack/react-query';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {\n //\n}\n\ninterface Payload {\n commentId: number;\n content: string;\n}\n\nexport function useUpdateComment() {\n return useMutation({\n mutationFn: (props: Payload) => updateComment(props),\n onSuccess: () => {\n toast(message('Comment updated'));\n queryClient.invalidateQueries({queryKey: ['comment']});\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction updateComment({commentId, content}: Payload): Promise {\n return apiClient.put(`comment/${commentId}`, {content}).then(r => r.data);\n}\n","import {useMutation} from '@tanstack/react-query';\nimport {apiClient} from '../../http/query-client';\nimport {BackendResponse} from '../../http/backend-response/backend-response';\nimport {toast} from '../../ui/toast/toast';\nimport {message} from '../../i18n/message';\nimport {showHttpErrorToast} from '../../utils/http/show-http-error-toast';\n\ninterface Response extends BackendResponse {\n //\n}\n\ninterface Payload {\n commentIds: number[];\n}\n\nexport function useRestoreComments() {\n return useMutation({\n mutationFn: (payload: Payload) => restoreComment(payload),\n onSuccess: (response, payload) => {\n toast(\n message('Restored [one 1 comment|other :count comments]', {\n values: {count: payload.commentIds.length},\n }),\n );\n },\n onError: err => showHttpErrorToast(err),\n });\n}\n\nfunction restoreComment({commentIds}: Payload): Promise {\n return apiClient.post('comment/restore', {commentIds}).then(r => r.data);\n}\n","import {queryClient} from '@common/http/query-client';\nimport {Button} from '@common/ui/buttons/button';\nimport {Trans} from '@common/i18n/trans';\nimport React from 'react';\nimport {ButtonVariant} from '@common/ui/buttons/get-shared-button-style';\nimport {ButtonSize} from '@common/ui/buttons/button-size';\nimport {useRestoreComments} from '@common/comments/requests/use-restore-comments';\n\ninterface Props {\n commentIds: number[];\n variant?: ButtonVariant;\n size?: ButtonSize;\n}\nexport function RestoreCommentsButton({\n commentIds,\n variant = 'outline',\n size = 'xs',\n}: Props) {\n const restoreComments = useRestoreComments();\n return (\n {\n restoreComments.mutate(\n {commentIds},\n {\n onSuccess: () => {\n queryClient.invalidateQueries({queryKey: ['comment']});\n },\n },\n );\n }}\n >\n \n \n );\n}\n","import {User} from '@common/auth/user';\nimport {Comment} from '@common/comments/comment';\nimport React, {Fragment, useContext, useState} from 'react';\nimport {Checkbox} from '@common/ui/forms/toggle/checkbox';\nimport {UserAvatar} from '@common/ui/images/user-avatar';\nimport {FormattedRelativeTime} from '@common/i18n/formatted-relative-time';\nimport {queryClient} from '@common/http/query-client';\nimport {DeleteCommentsButton} from '@common/comments/comments-datatable-page/delete-comments-button';\nimport {Button} from '@common/ui/buttons/button';\nimport {Trans} from '@common/i18n/trans';\nimport {useUpdateComment} from '@common/comments/requests/use-update-comment';\nimport {TextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\nimport {Link} from 'react-router-dom';\nimport {LinkStyle} from '@common/ui/buttons/external-link';\nimport clsx from 'clsx';\nimport {RestoreCommentsButton} from '@common/comments/comments-datatable-page/restore-comments-button';\nimport {NormalizedModel} from '@common/datatable/filters/normalized-model';\n\ninterface Props {\n comment: Comment;\n isSelected: boolean;\n onToggle: () => void;\n}\nexport function CommentDatatableItem({comment, isSelected, onToggle}: Props) {\n const [isEditing, setIsEditing] = useState(false);\n return (\n
\n {comment.commentable && (\n \n )}\n
\n \n
\n \n {isEditing ? (\n {\n setIsEditing(false);\n if (isSaved) {\n queryClient.invalidateQueries({queryKey: ['comment']});\n }\n }}\n />\n ) : (\n \n
{comment.content}
\n
\n
\n {comment.deleted ? (\n \n ) : (\n \n )}\n {\n setIsEditing(true);\n }}\n >\n \n \n
\n
\n \n
\n
\n
\n )}\n
\n
\n
\n );\n}\n\ninterface CommentableHeaderProps {\n isSelected: boolean;\n onToggle: Props['onToggle'];\n commentable: NormalizedModel;\n}\nfunction CommentableHeader({\n isSelected,\n onToggle,\n commentable,\n}: CommentableHeaderProps) {\n return (\n
\n
\n onToggle()} />\n
\n {commentable.image && (\n \n )}\n
{commentable.name}
\n
({commentable.model_type})
\n
\n );\n}\n\ninterface CommentHeaderProps {\n comment: Comment;\n}\nfunction CommentHeader({comment}: CommentHeaderProps) {\n return (\n
\n
\n {comment.user && (\n \n )}\n
\n
\n \n {comment.user && (\n \n )}\n
\n );\n}\n\ninterface EditCommentFormProps {\n comment: Comment;\n onClose: (saved: boolean) => void;\n}\nfunction EditCommentForm({comment, onClose}: EditCommentFormProps) {\n const [content, setContent] = useState(comment.content);\n const updateComment = useUpdateComment();\n return (\n {\n e.preventDefault();\n updateComment.mutate(\n {commentId: comment.id, content},\n {onSuccess: () => onClose(true)},\n );\n }}\n >\n setContent(e.target.value)}\n />\n \n \n \n onClose(false)}\n disabled={updateComment.isPending}\n >\n \n \n \n );\n}\n\ninterface UserDisplayNameProps {\n user: User;\n show: 'display_name' | 'email';\n}\nfunction UserDisplayName({user, show}: UserDisplayNameProps) {\n const {auth} = useContext(SiteConfigContext);\n if (auth.getUserProfileLink) {\n return (\n \n {user[show]}\n \n );\n }\n return
{user[show]}
;\n}\n","export default \"__VITE_ASSET__a97a5552__\"","import {\n BackendFilter,\n FilterControlType,\n FilterOperator,\n} from '@common/datatable/filters/backend-filter';\nimport {message} from '@common/i18n/message';\nimport {USER_MODEL} from '@common/auth/user';\nimport {\n createdAtFilter,\n updatedAtFilter,\n} from '@common/datatable/filters/timestamp-filters';\n\nexport const CommentsDatatableFilters: BackendFilter[] = [\n {\n key: 'deleted',\n label: message('Status'),\n description: message('Whether comment is active or deleted'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.Select,\n defaultValue: '01',\n options: [\n {\n key: '01',\n label: message('Active'),\n value: false,\n },\n {\n key: '02',\n label: message('Deleted'),\n value: true,\n },\n ],\n },\n },\n {\n key: 'reports',\n label: message('Reported'),\n description: message('Show only reported comments'),\n defaultOperator: FilterOperator.has,\n control: {\n type: FilterControlType.BooleanToggle,\n defaultValue: '*',\n },\n },\n {\n key: 'user_id',\n label: message('User'),\n description: message('User comment was created by'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.SelectModel,\n model: USER_MODEL,\n },\n },\n createdAtFilter({\n description: message('Date comment was created'),\n }),\n updatedAtFilter({\n description: message('Date comment was last updated'),\n }),\n];\n","import React, {useCallback, useMemo, useState} from 'react';\nimport {Trans} from '@common/i18n/trans';\nimport clsx from 'clsx';\nimport {StaticPageTitle} from '@common/seo/static-page-title';\nimport {DataTableHeader} from '@common/datatable/data-table-header';\nimport {useBackendFilterUrlParams} from '@common/datatable/filters/backend-filter-url-params';\nimport {\n GetDatatableDataParams,\n useDatatableData,\n} from '@common/datatable/requests/paginated-resources';\nimport {Comment} from '@common/comments/comment';\nimport {FilterList} from '@common/datatable/filters/filter-list/filter-list';\nimport {SelectedStateDatatableHeader} from '@common/datatable/selected-state-datatable-header';\nimport {AnimatePresence} from 'framer-motion';\nimport {DeleteCommentsButton} from '@common/comments/comments-datatable-page/delete-comments-button';\nimport {CommentDatatableItem} from '@common/comments/comments-datatable-page/comment-datatable-item';\nimport {DataTablePaginationFooter} from '@common/datatable/data-table-pagination-footer';\nimport {DataTableEmptyStateMessage} from '@common/datatable/page/data-table-emty-state-message';\nimport publicDiscussionsImage from './public-discussion.svg';\nimport {FullPageLoader} from '@common/ui/progress/full-page-loader';\nimport {Commentable} from '@common/comments/commentable';\nimport {CommentsDatatableFilters} from '@common/comments/comments-datatable-page/comments-datatable-filters';\n\ninterface Props {\n hideTitle?: boolean;\n commentable?: Commentable;\n}\nexport function CommentsDatatablePage({hideTitle, commentable}: Props) {\n const filters = useMemo(() => {\n return CommentsDatatableFilters.filter(\n f => f.key !== 'commentable_id' || !commentable,\n );\n }, [commentable]);\n const {encodedFilters} = useBackendFilterUrlParams(filters);\n const [params, setParams] = useState({perPage: 15});\n const [selectedComments, setSelectedComments] = useState([]);\n const query = useDatatableData(\n 'comment',\n {\n ...params,\n with: 'commentable',\n withCount: 'reports',\n filters: encodedFilters,\n commentable_type: commentable?.model_type,\n commentable_id: commentable?.id,\n },\n undefined,\n () => {\n setSelectedComments([]);\n },\n );\n\n const toggleComment = useCallback(\n (id: number) => {\n const newValues = [...selectedComments];\n if (!newValues.includes(id)) {\n newValues.push(id);\n } else {\n const index = newValues.indexOf(id);\n newValues.splice(index, 1);\n }\n setSelectedComments(newValues);\n },\n [selectedComments, setSelectedComments],\n );\n\n const isFiltering = !!(params.query || params.filters || encodedFilters);\n const pagination = query.data?.pagination;\n\n return (\n
\n
\n \n \n \n {!hideTitle && (\n

\n \n

\n )}\n
\n
\n \n {selectedComments.length ? (\n \n }\n key=\"selected\"\n />\n ) : (\n setParams({...params, query})}\n key=\"default\"\n />\n )}\n \n \n\n {query.isLoading ? (\n \n ) : (\n
\n {pagination?.data.map(comment => (\n toggleComment(comment.id)}\n />\n ))}\n
\n )}\n\n {(query.isFetched || query.isPlaceholderData) &&\n !pagination?.data.length ? (\n }\n filteringTitle={}\n />\n ) : undefined}\n\n setParams({...params, page})}\n onPerPageChange={perPage => setParams({...params, perPage})}\n />\n
\n
\n );\n}\n","export default \"__VITE_ASSET__5161f729__\"","import {DialogTrigger} from '@common/ui/overlays/dialog/dialog-trigger';\nimport {Button} from '@common/ui/buttons/button';\nimport {Trans} from '@common/i18n/trans';\nimport {ConfirmationDialog} from '@common/ui/overlays/dialog/confirmation-dialog';\nimport React from 'react';\nimport {ButtonVariant} from '@common/ui/buttons/get-shared-button-style';\nimport {ButtonSize} from '@common/ui/buttons/button-size';\nimport {useDeleteReviews} from '@app/reviews/requests/use-delete-reviews';\n\ninterface Props {\n reviewIds: number[];\n variant?: ButtonVariant;\n size?: ButtonSize;\n}\nexport function DeleteReviewsButton({\n reviewIds,\n variant = 'outline',\n size = 'xs',\n}: Props) {\n const deleteReviews = useDeleteReviews();\n return (\n {\n if (isConfirmed) {\n deleteReviews.mutate({reviewIds});\n }\n }}\n >\n \n \n \n \n }\n body={\n reviewIds.length > 1 ? (\n \n ) : (\n \n )\n }\n confirm={}\n />\n \n );\n}\n","import {BackendResponse} from '@common/http/backend-response/backend-response';\nimport {useMutation} from '@tanstack/react-query';\nimport {apiClient, queryClient} from '@common/http/query-client';\nimport {showHttpErrorToast} from '@common/utils/http/show-http-error-toast';\nimport {Review} from '@app/titles/models/review';\nimport {UseFormReturn} from 'react-hook-form';\nimport {onFormQueryError} from '@common/errors/on-form-query-error';\nimport {CreateReviewPayload} from '@app/reviews/requests/use-create-review';\nimport {toast} from '@common/ui/toast/toast';\nimport {message} from '@common/i18n/message';\n\ninterface Response extends BackendResponse {\n review: Review;\n}\n\nexport function useUpdateReview(\n review: Review,\n form?: UseFormReturn,\n) {\n return useMutation({\n mutationFn: (payload: CreateReviewPayload) => updateReview(review, payload),\n onSuccess: () => {\n queryClient.invalidateQueries({queryKey: ['reviews']});\n toast(message('Review updated'));\n },\n onError: r => (form ? onFormQueryError(r, form) : showHttpErrorToast(r)),\n });\n}\n\nfunction updateReview(\n review: Review,\n payload: CreateReviewPayload,\n): Promise {\n return apiClient\n .put(`reviews/${review.id}`, {\n score: payload.score,\n title: payload.title,\n body: payload.body,\n })\n .then(r => r.data);\n}\n","import {User} from '@common/auth/user';\nimport React, {Fragment, useContext, useState} from 'react';\nimport {Checkbox} from '@common/ui/forms/toggle/checkbox';\nimport {UserAvatar} from '@common/ui/images/user-avatar';\nimport {FormattedRelativeTime} from '@common/i18n/formatted-relative-time';\nimport {queryClient} from '@common/http/query-client';\nimport {Button} from '@common/ui/buttons/button';\nimport {Trans} from '@common/i18n/trans';\nimport {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';\nimport {SiteConfigContext} from '@common/core/settings/site-config-context';\nimport {Link} from 'react-router-dom';\nimport {LinkStyle} from '@common/ui/buttons/external-link';\nimport {NormalizedModel} from '@common/datatable/filters/normalized-model';\nimport {Review} from '@app/titles/models/review';\nimport {TitleRating} from '@app/reviews/title-rating';\nimport {useUpdateReview} from '@app/admin/reviews/requests/use-update-review';\nimport {useForm} from 'react-hook-form';\nimport {CreateReviewPayload} from '@app/reviews/requests/use-create-review';\nimport {Form} from '@common/ui/forms/form';\nimport {StarSelector} from '@app/reviews/review-list/star-selector';\nimport {DeleteReviewsButton} from '@app/admin/reviews/delete-reviews-button';\nimport {BulletSeparatedItems} from '@app/titles/bullet-separated-items';\n\ninterface Props {\n review: Review;\n isSelected: boolean;\n onToggle: () => void;\n}\nexport function ReviewDatatableItem({review, isSelected, onToggle}: Props) {\n const [isEditing, setIsEditing] = useState(false);\n\n const helpfulCount = review.helpful_count || 1;\n const totalFeedbackCount =\n review.helpful_count + review.not_helpful_count || 1;\n\n return (\n
\n {review.reviewable && (\n \n )}\n
\n \n
\n \n {isEditing ? (\n {\n setIsEditing(false);\n if (isSaved) {\n queryClient.invalidateQueries({queryKey: ['comment']});\n }\n }}\n />\n ) : (\n \n
\n \n {review.title && (\n
\n {review.title}\n
\n )}\n
\n {review.body}\n
\n
\n \n \n {review.reports_count ? (\n \n ) : null}\n \n
\n
\n
\n \n setIsEditing(true)}\n >\n \n \n
\n
\n )}\n
\n
\n
\n );\n}\n\ninterface ReviewableHeaderProps {\n isSelected: boolean;\n onToggle: Props['onToggle'];\n reviewable: NormalizedModel;\n}\nfunction ReviewableHeader({\n isSelected,\n onToggle,\n reviewable,\n}: ReviewableHeaderProps) {\n return (\n
\n
\n onToggle()} />\n
\n {reviewable.image && (\n \n )}\n
{reviewable.name}
\n
({reviewable.model_type})
\n
\n );\n}\n\ninterface CommentHeaderProps {\n review: Review;\n}\nfunction ReviewHeader({review}: CommentHeaderProps) {\n return (\n
\n
\n {review.user && (\n \n )}\n
\n
\n \n {review.user && (\n \n )}\n
\n );\n}\n\ninterface EditReviewFormProps {\n review: Review;\n onClose: (saved: boolean) => void;\n}\nfunction EditReviewForm({review, onClose}: EditReviewFormProps) {\n const [content, setContent] = useState(review.body);\n const updateReview = useUpdateReview(review);\n const form = useForm({\n defaultValues: {\n score: review.score,\n title: review.title,\n body: review.body,\n },\n });\n return (\n {\n updateReview.mutate(newValues, {onSuccess: () => onClose(true)});\n }}\n >\n {\n form.setValue('score', newScore);\n }}\n />\n }\n labelSuffix={}\n autoFocus\n minLength={10}\n required\n />\n }\n labelSuffix={}\n inputElementType=\"textarea\"\n rows={5}\n minLength={100}\n required\n />\n \n \n \n onClose(false)}\n disabled={updateReview.isPending}\n >\n \n \n \n );\n}\n\ninterface UserDisplayNameProps {\n user: User;\n show: 'display_name' | 'email';\n}\nfunction UserDisplayName({user, show}: UserDisplayNameProps) {\n const {auth} = useContext(SiteConfigContext);\n if (auth.getUserProfileLink) {\n return (\n \n {user[show]}\n \n );\n }\n return
{user[show]}
;\n}\n","import {\n ALL_PRIMITIVE_OPERATORS,\n BackendFilter,\n FilterControlType,\n FilterOperator,\n} from '@common/datatable/filters/backend-filter';\nimport {message} from '@common/i18n/message';\nimport {USER_MODEL} from '@common/auth/user';\nimport {\n createdAtFilter,\n updatedAtFilter,\n} from '@common/datatable/filters/timestamp-filters';\nimport {TITLE_MODEL} from '@app/titles/models/title';\n\nexport const ReviewsDatatableFilters: BackendFilter[] = [\n {\n key: 'user_id',\n label: message('User'),\n description: message('User review was created by'),\n defaultOperator: FilterOperator.eq,\n control: {\n type: FilterControlType.SelectModel,\n model: USER_MODEL,\n },\n },\n {\n key: 'reviewable_id',\n label: message('Title'),\n description: message('Movie or series review was created for'),\n defaultOperator: FilterOperator.eq,\n extraFilters: [\n {\n key: 'reviewable_type',\n operator: FilterOperator.eq,\n value: 'App\\\\Title',\n },\n ],\n control: {\n type: FilterControlType.SelectModel,\n model: TITLE_MODEL,\n },\n },\n {\n key: 'score',\n label: message('Score'),\n description: message('Review score'),\n defaultOperator: FilterOperator.gte,\n operators: ALL_PRIMITIVE_OPERATORS,\n control: {\n type: FilterControlType.Input,\n inputType: 'number',\n minValue: 1,\n maxValue: 10,\n defaultValue: 7,\n },\n },\n {\n key: 'helpful_count',\n label: message('Helpful count'),\n description: message('How many users found this review helpful'),\n defaultOperator: FilterOperator.gte,\n operators: ALL_PRIMITIVE_OPERATORS,\n control: {\n type: FilterControlType.Input,\n inputType: 'number',\n minValue: 1,\n defaultValue: 10,\n },\n },\n {\n key: 'not_helpful_count',\n label: message('Not helpful count'),\n description: message('How many users found this review not helpful'),\n defaultOperator: FilterOperator.gte,\n operators: ALL_PRIMITIVE_OPERATORS,\n control: {\n type: FilterControlType.Input,\n inputType: 'number',\n minValue: 1,\n defaultValue: 10,\n },\n },\n createdAtFilter({\n description: message('Date review was created'),\n }),\n updatedAtFilter({\n description: message('Date review was last updated'),\n }),\n];\n","import React, { useCallback, useMemo, useState } from \"react\";\nimport { Trans } from \"@common/i18n/trans\";\nimport clsx from \"clsx\";\nimport { StaticPageTitle } from \"@common/seo/static-page-title\";\nimport { DataTableHeader } from \"@common/datatable/data-table-header\";\nimport {\n useBackendFilterUrlParams\n} from \"@common/datatable/filters/backend-filter-url-params\";\nimport {\n GetDatatableDataParams,\n useDatatableData\n} from \"@common/datatable/requests/paginated-resources\";\nimport { FilterList } from \"@common/datatable/filters/filter-list/filter-list\";\nimport {\n SelectedStateDatatableHeader\n} from \"@common/datatable/selected-state-datatable-header\";\nimport { AnimatePresence } from \"framer-motion\";\nimport {\n DataTablePaginationFooter\n} from \"@common/datatable/data-table-pagination-footer\";\nimport {\n DataTableEmptyStateMessage\n} from \"@common/datatable/page/data-table-emty-state-message\";\nimport reviewsImage from \"./reviews.svg\";\nimport { FullPageLoader } from \"@common/ui/progress/full-page-loader\";\nimport { Review } from \"@app/titles/models/review\";\nimport { DeleteReviewsButton } from \"@app/admin/reviews/delete-reviews-button\";\nimport { ReviewDatatableItem } from \"@app/admin/reviews/review-datatable-item\";\nimport {\n ReviewsDatatableFilters\n} from \"@app/admin/reviews/reviews-datatable-filters\";\nimport {\n ReviewListSortButton\n} from \"@app/reviews/review-list/review-list-sort-button\";\nimport { Reviewable } from \"@app/reviews/reviewable\";\n\ninterface Props {\n hideTitle?: boolean;\n reviewable?: Reviewable;\n}\nexport function ReviewsDatatablePage({hideTitle, reviewable}: Props) {\n const filters = useMemo(() => {\n return ReviewsDatatableFilters.filter(\n f => f.key !== 'reviewable_id' || !reviewable,\n );\n }, [reviewable]);\n const {encodedFilters} = useBackendFilterUrlParams(filters);\n const [params, setParams] = useState({perPage: 15});\n const [selectedReviews, setSelectedReviews] = useState([]);\n const [sort, setSort] = useState('created_at:desc');\n const [orderBy, orderDir] = sort.split(':');\n\n const query = useDatatableData('reviews', {\n ...params,\n orderBy,\n orderDir: orderDir as 'asc' | 'desc',\n with: 'reviewable,user',\n filters: encodedFilters,\n reviewable_type: reviewable?.model_type,\n reviewable_id: reviewable?.id,\n }, undefined, () => {\n setSelectedReviews([]);\n });\n\n const toggleReview = useCallback(\n (id: number) => {\n const newValues = [...selectedReviews];\n if (!newValues.includes(id)) {\n newValues.push(id);\n } else {\n const index = newValues.indexOf(id);\n newValues.splice(index, 1);\n }\n setSelectedReviews(newValues);\n },\n [selectedReviews, setSelectedReviews],\n );\n\n const isFiltering = !!(params.query || params.filters || encodedFilters);\n const pagination = query.data?.pagination;\n\n return (\n
\n
\n \n \n \n {!hideTitle && (\n

\n \n

\n )}\n
\n
\n \n {selectedReviews.length ? (\n \n }\n key=\"selected\"\n />\n ) : (\n setParams({...params, query})}\n actions={\n setSort(newSort)}\n color=\"primary\"\n showReportsItem\n />\n }\n />\n )}\n \n \n\n {query.isLoading ? (\n \n ) : (\n
\n {pagination?.data.map(review => (\n toggleReview(review.id)}\n />\n ))}\n
\n )}\n\n {(query.isFetched || query.isPlaceholderData) &&\n !pagination?.data.length ? (\n }\n filteringTitle={}\n />\n ) : undefined}\n\n setParams({...params, page})}\n onPerPageChange={perPage => setParams({...params, perPage})}\n />\n
\n
\n );\n}\n","export default \"__VITE_ASSET__abdf0323__\"","import {CheckIcon} from '@common/icons/material/Check';\nimport {CloseIcon} from '@common/icons/material/Close';\nimport React from 'react';\n\ninterface BooleanIndicatorProps {\n value: boolean;\n}\nexport function BooleanIndicator({value}: BooleanIndicatorProps) {\n if (value) {\n return ;\n }\n return ;\n}\n","import {createSvgIcon} from '../create-svg-icon';\n\nexport const BarChartIcon = createSvgIcon(\n \n, 'BarChartOutlined');\n","import {ColumnConfig} from '@common/datatable/column-config';\nimport {Trans} from '@common/i18n/trans';\nimport {FormattedDate} from '@common/i18n/formatted-date';\nimport {Link} from 'react-router-dom';\nimport {IconButton} from '@common/ui/buttons/icon-button';\nimport {EditIcon} from '@common/icons/material/Edit';\nimport React, {Fragment} from 'react';\nimport {Video} from '@app/titles/models/video';\nimport {BooleanIndicator} from '@common/datatable/column-templates/boolean-indicator';\nimport {FormattedNumber} from '@common/i18n/formatted-number';\nimport {TitlePoster} from '@app/titles/title-poster/title-poster';\nimport {BarChartIcon} from '@common/icons/material/BarChart';\nimport {CompactSeasonEpisode} from '@app/episodes/compact-season-episode';\nimport {getWatchLink} from '@app/videos/watch-page/get-watch-link';\n\nexport const VideosDatatableColumns: ColumnConfig