feat(portal): login, dashboard, event switcher, password reset flow
Made-with: Cursor
This commit is contained in:
349
apps/portal/components.d.ts
vendored
349
apps/portal/components.d.ts
vendored
@@ -7,11 +7,6 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AddAuthenticatorAppDialog: typeof import('./src/components/dialogs/AddAuthenticatorAppDialog.vue')['default']
|
||||
AddEditAddressDialog: typeof import('./src/components/dialogs/AddEditAddressDialog.vue')['default']
|
||||
AddEditPermissionDialog: typeof import('./src/components/dialogs/AddEditPermissionDialog.vue')['default']
|
||||
AddEditRoleDialog: typeof import('./src/components/dialogs/AddEditRoleDialog.vue')['default']
|
||||
AddPaymentMethodDialog: typeof import('./src/components/dialogs/AddPaymentMethodDialog.vue')['default']
|
||||
AppAutocomplete: typeof import('./src/@core/components/app-form-elements/AppAutocomplete.vue')['default']
|
||||
AppBarSearch: typeof import('./src/@core/components/AppBarSearch.vue')['default']
|
||||
AppCardActions: typeof import('./src/@core/components/cards/AppCardActions.vue')['default']
|
||||
@@ -20,19 +15,14 @@ declare module 'vue' {
|
||||
AppDateTimePicker: typeof import('./src/@core/components/app-form-elements/AppDateTimePicker.vue')['default']
|
||||
AppDrawerHeaderSection: typeof import('./src/@core/components/AppDrawerHeaderSection.vue')['default']
|
||||
AppLoadingIndicator: typeof import('./src/components/AppLoadingIndicator.vue')['default']
|
||||
AppPricing: typeof import('./src/components/AppPricing.vue')['default']
|
||||
AppSearchHeader: typeof import('./src/components/AppSearchHeader.vue')['default']
|
||||
AppSelect: typeof import('./src/@core/components/app-form-elements/AppSelect.vue')['default']
|
||||
AppStepper: typeof import('./src/@core/components/AppStepper.vue')['default']
|
||||
AppTextarea: typeof import('./src/@core/components/app-form-elements/AppTextarea.vue')['default']
|
||||
AppTextField: typeof import('./src/@core/components/app-form-elements/AppTextField.vue')['default']
|
||||
BuyNow: typeof import('./src/@core/components/BuyNow.vue')['default']
|
||||
CardAddEditDialog: typeof import('./src/components/dialogs/CardAddEditDialog.vue')['default']
|
||||
CardStatisticsHorizontal: typeof import('./src/@core/components/cards/CardStatisticsHorizontal.vue')['default']
|
||||
CardStatisticsVertical: typeof import('./src/@core/components/cards/CardStatisticsVertical.vue')['default']
|
||||
CardStatisticsVerticalSimple: typeof import('./src/@core/components/CardStatisticsVerticalSimple.vue')['default']
|
||||
ConfirmDialog: typeof import('./src/components/dialogs/ConfirmDialog.vue')['default']
|
||||
CreateAppDialog: typeof import('./src/components/dialogs/CreateAppDialog.vue')['default']
|
||||
CustomCheckboxes: typeof import('./src/@core/components/app-form-elements/CustomCheckboxes.vue')['default']
|
||||
CustomCheckboxesWithIcon: typeof import('./src/@core/components/app-form-elements/CustomCheckboxesWithIcon.vue')['default']
|
||||
CustomCheckboxesWithImage: typeof import('./src/@core/components/app-form-elements/CustomCheckboxesWithImage.vue')['default']
|
||||
@@ -40,356 +30,21 @@ declare module 'vue' {
|
||||
CustomRadios: typeof import('./src/@core/components/app-form-elements/CustomRadios.vue')['default']
|
||||
CustomRadiosWithIcon: typeof import('./src/@core/components/app-form-elements/CustomRadiosWithIcon.vue')['default']
|
||||
CustomRadiosWithImage: typeof import('./src/@core/components/app-form-elements/CustomRadiosWithImage.vue')['default']
|
||||
DemoAlertBasic: typeof import('./src/views/demos/components/alert/DemoAlertBasic.vue')['default']
|
||||
DemoAlertBorder: typeof import('./src/views/demos/components/alert/DemoAlertBorder.vue')['default']
|
||||
DemoAlertClosable: typeof import('./src/views/demos/components/alert/DemoAlertClosable.vue')['default']
|
||||
DemoAlertColoredBorder: typeof import('./src/views/demos/components/alert/DemoAlertColoredBorder.vue')['default']
|
||||
DemoAlertColors: typeof import('./src/views/demos/components/alert/DemoAlertColors.vue')['default']
|
||||
DemoAlertDensity: typeof import('./src/views/demos/components/alert/DemoAlertDensity.vue')['default']
|
||||
DemoAlertElevation: typeof import('./src/views/demos/components/alert/DemoAlertElevation.vue')['default']
|
||||
DemoAlertIcons: typeof import('./src/views/demos/components/alert/DemoAlertIcons.vue')['default']
|
||||
DemoAlertOutlined: typeof import('./src/views/demos/components/alert/DemoAlertOutlined.vue')['default']
|
||||
DemoAlertProminent: typeof import('./src/views/demos/components/alert/DemoAlertProminent.vue')['default']
|
||||
DemoAlertTonal: typeof import('./src/views/demos/components/alert/DemoAlertTonal.vue')['default']
|
||||
DemoAlertType: typeof import('./src/views/demos/components/alert/DemoAlertType.vue')['default']
|
||||
DemoAlertVModelSupport: typeof import('./src/views/demos/components/alert/DemoAlertVModelSupport.vue')['default']
|
||||
DemoAutocompleteAsyncItems: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteAsyncItems.vue')['default']
|
||||
DemoAutocompleteBasic: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteBasic.vue')['default']
|
||||
DemoAutocompleteChips: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteChips.vue')['default']
|
||||
DemoAutocompleteClearable: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteClearable.vue')['default']
|
||||
DemoAutocompleteCustomFilter: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteCustomFilter.vue')['default']
|
||||
DemoAutocompleteDensity: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteDensity.vue')['default']
|
||||
DemoAutocompleteMultiple: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteMultiple.vue')['default']
|
||||
DemoAutocompleteSlots: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteSlots.vue')['default']
|
||||
DemoAutocompleteStateSelector: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteStateSelector.vue')['default']
|
||||
DemoAutocompleteValidation: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteValidation.vue')['default']
|
||||
DemoAutocompleteVariant: typeof import('./src/views/demos/forms/form-elements/autocomplete/DemoAutocompleteVariant.vue')['default']
|
||||
DemoAvatarColors: typeof import('./src/views/demos/components/avatar/DemoAvatarColors.vue')['default']
|
||||
DemoAvatarGroup: typeof import('./src/views/demos/components/avatar/DemoAvatarGroup.vue')['default']
|
||||
DemoAvatarIcons: typeof import('./src/views/demos/components/avatar/DemoAvatarIcons.vue')['default']
|
||||
DemoAvatarImages: typeof import('./src/views/demos/components/avatar/DemoAvatarImages.vue')['default']
|
||||
DemoAvatarRounded: typeof import('./src/views/demos/components/avatar/DemoAvatarRounded.vue')['default']
|
||||
DemoAvatarSizes: typeof import('./src/views/demos/components/avatar/DemoAvatarSizes.vue')['default']
|
||||
DemoAvatarTonal: typeof import('./src/views/demos/components/avatar/DemoAvatarTonal.vue')['default']
|
||||
DemoBadgeAvatarStatus: typeof import('./src/views/demos/components/badge/DemoBadgeAvatarStatus.vue')['default']
|
||||
DemoBadgeColor: typeof import('./src/views/demos/components/badge/DemoBadgeColor.vue')['default']
|
||||
DemoBadgeDynamicNotifications: typeof import('./src/views/demos/components/badge/DemoBadgeDynamicNotifications.vue')['default']
|
||||
DemoBadgeIcon: typeof import('./src/views/demos/components/badge/DemoBadgeIcon.vue')['default']
|
||||
DemoBadgeMaximumValue: typeof import('./src/views/demos/components/badge/DemoBadgeMaximumValue.vue')['default']
|
||||
DemoBadgePosition: typeof import('./src/views/demos/components/badge/DemoBadgePosition.vue')['default']
|
||||
DemoBadgeShowOnHover: typeof import('./src/views/demos/components/badge/DemoBadgeShowOnHover.vue')['default']
|
||||
DemoBadgeStyle: typeof import('./src/views/demos/components/badge/DemoBadgeStyle.vue')['default']
|
||||
DemoBadgeTabs: typeof import('./src/views/demos/components/badge/DemoBadgeTabs.vue')['default']
|
||||
DemoBadgeTonal: typeof import('./src/views/demos/components/badge/DemoBadgeTonal.vue')['default']
|
||||
DemoButtonBlock: typeof import('./src/views/demos/components/button/DemoButtonBlock.vue')['default']
|
||||
DemoButtonColors: typeof import('./src/views/demos/components/button/DemoButtonColors.vue')['default']
|
||||
DemoButtonFlat: typeof import('./src/views/demos/components/button/DemoButtonFlat.vue')['default']
|
||||
DemoButtonGroup: typeof import('./src/views/demos/components/button/DemoButtonGroup.vue')['default']
|
||||
DemoButtonIcon: typeof import('./src/views/demos/components/button/DemoButtonIcon.vue')['default']
|
||||
DemoButtonIconOnly: typeof import('./src/views/demos/components/button/DemoButtonIconOnly.vue')['default']
|
||||
DemoButtonLink: typeof import('./src/views/demos/components/button/DemoButtonLink.vue')['default']
|
||||
DemoButtonLoaders: typeof import('./src/views/demos/components/button/DemoButtonLoaders.vue')['default']
|
||||
DemoButtonOutlined: typeof import('./src/views/demos/components/button/DemoButtonOutlined.vue')['default']
|
||||
DemoButtonPlain: typeof import('./src/views/demos/components/button/DemoButtonPlain.vue')['default']
|
||||
DemoButtonRounded: typeof import('./src/views/demos/components/button/DemoButtonRounded.vue')['default']
|
||||
DemoButtonRouter: typeof import('./src/views/demos/components/button/DemoButtonRouter.vue')['default']
|
||||
DemoButtonSizing: typeof import('./src/views/demos/components/button/DemoButtonSizing.vue')['default']
|
||||
DemoButtonText: typeof import('./src/views/demos/components/button/DemoButtonText.vue')['default']
|
||||
DemoButtonTonal: typeof import('./src/views/demos/components/button/DemoButtonTonal.vue')['default']
|
||||
DemoCheckboxBasic: typeof import('./src/views/demos/forms/form-elements/checkbox/DemoCheckboxBasic.vue')['default']
|
||||
DemoCheckboxCheckboxValue: typeof import('./src/views/demos/forms/form-elements/checkbox/DemoCheckboxCheckboxValue.vue')['default']
|
||||
DemoCheckboxColors: typeof import('./src/views/demos/forms/form-elements/checkbox/DemoCheckboxColors.vue')['default']
|
||||
DemoCheckboxDensity: typeof import('./src/views/demos/forms/form-elements/checkbox/DemoCheckboxDensity.vue')['default']
|
||||
DemoCheckboxIcon: typeof import('./src/views/demos/forms/form-elements/checkbox/DemoCheckboxIcon.vue')['default']
|
||||
DemoCheckboxInlineTextField: typeof import('./src/views/demos/forms/form-elements/checkbox/DemoCheckboxInlineTextField.vue')['default']
|
||||
DemoCheckboxLabelSlot: typeof import('./src/views/demos/forms/form-elements/checkbox/DemoCheckboxLabelSlot.vue')['default']
|
||||
DemoCheckboxModelAsArray: typeof import('./src/views/demos/forms/form-elements/checkbox/DemoCheckboxModelAsArray.vue')['default']
|
||||
DemoCheckboxStates: typeof import('./src/views/demos/forms/form-elements/checkbox/DemoCheckboxStates.vue')['default']
|
||||
DemoChipClosable: typeof import('./src/views/demos/components/chip/DemoChipClosable.vue')['default']
|
||||
DemoChipColor: typeof import('./src/views/demos/components/chip/DemoChipColor.vue')['default']
|
||||
DemoChipElevated: typeof import('./src/views/demos/components/chip/DemoChipElevated.vue')['default']
|
||||
DemoChipExpandable: typeof import('./src/views/demos/components/chip/DemoChipExpandable.vue')['default']
|
||||
DemoChipInSelects: typeof import('./src/views/demos/components/chip/DemoChipInSelects.vue')['default']
|
||||
DemoChipOutlined: typeof import('./src/views/demos/components/chip/DemoChipOutlined.vue')['default']
|
||||
DemoChipRounded: typeof import('./src/views/demos/components/chip/DemoChipRounded.vue')['default']
|
||||
DemoChipSizes: typeof import('./src/views/demos/components/chip/DemoChipSizes.vue')['default']
|
||||
DemoChipWithAvatar: typeof import('./src/views/demos/components/chip/DemoChipWithAvatar.vue')['default']
|
||||
DemoChipWithIcon: typeof import('./src/views/demos/components/chip/DemoChipWithIcon.vue')['default']
|
||||
DemoComboboxBasic: typeof import('./src/views/demos/forms/form-elements/combobox/DemoComboboxBasic.vue')['default']
|
||||
DemoComboboxClearable: typeof import('./src/views/demos/forms/form-elements/combobox/DemoComboboxClearable.vue')['default']
|
||||
DemoComboboxDensity: typeof import('./src/views/demos/forms/form-elements/combobox/DemoComboboxDensity.vue')['default']
|
||||
DemoComboboxMultiple: typeof import('./src/views/demos/forms/form-elements/combobox/DemoComboboxMultiple.vue')['default']
|
||||
DemoComboboxNoDataWithChips: typeof import('./src/views/demos/forms/form-elements/combobox/DemoComboboxNoDataWithChips.vue')['default']
|
||||
DemoComboboxVariant: typeof import('./src/views/demos/forms/form-elements/combobox/DemoComboboxVariant.vue')['default']
|
||||
DemoCustomInputCustomCheckboxes: typeof import('./src/views/demos/forms/form-elements/custom-input/DemoCustomInputCustomCheckboxes.vue')['default']
|
||||
DemoCustomInputCustomCheckboxesWithIcon: typeof import('./src/views/demos/forms/form-elements/custom-input/DemoCustomInputCustomCheckboxesWithIcon.vue')['default']
|
||||
DemoCustomInputCustomCheckboxesWithImage: typeof import('./src/views/demos/forms/form-elements/custom-input/DemoCustomInputCustomCheckboxesWithImage.vue')['default']
|
||||
DemoCustomInputCustomRadios: typeof import('./src/views/demos/forms/form-elements/custom-input/DemoCustomInputCustomRadios.vue')['default']
|
||||
DemoCustomInputCustomRadiosWithIcon: typeof import('./src/views/demos/forms/form-elements/custom-input/DemoCustomInputCustomRadiosWithIcon.vue')['default']
|
||||
DemoCustomInputCustomRadiosWithImage: typeof import('./src/views/demos/forms/form-elements/custom-input/DemoCustomInputCustomRadiosWithImage.vue')['default']
|
||||
DemoDataTableBasic: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableBasic.vue')['default']
|
||||
DemoDataTableCellSlot: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableCellSlot.vue')['default']
|
||||
DemoDataTableDense: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableDense.vue')['default']
|
||||
DemoDataTableExpandableRows: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableExpandableRows.vue')['default']
|
||||
DemoDataTableExternalPagination: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableExternalPagination.vue')['default']
|
||||
DemoDataTableFixedHeader: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableFixedHeader.vue')['default']
|
||||
DemoDataTableGroupingRows: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableGroupingRows.vue')['default']
|
||||
DemoDataTableKitchenSink: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableKitchenSink.vue')['default']
|
||||
DemoDataTableRowEditingViaDialog: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableRowEditingViaDialog.vue')['default']
|
||||
DemoDataTableRowSelection: typeof import('./src/views/demos/forms/tables/data-table/DemoDataTableRowSelection.vue')['default']
|
||||
DemoDateTimePickerBasic: typeof import('./src/views/demos/forms/form-elements/date-time-picker/DemoDateTimePickerBasic.vue')['default']
|
||||
DemoDateTimePickerDateAndTime: typeof import('./src/views/demos/forms/form-elements/date-time-picker/DemoDateTimePickerDateAndTime.vue')['default']
|
||||
DemoDateTimePickerDisabledRange: typeof import('./src/views/demos/forms/form-elements/date-time-picker/DemoDateTimePickerDisabledRange.vue')['default']
|
||||
DemoDateTimePickerHumanFriendly: typeof import('./src/views/demos/forms/form-elements/date-time-picker/DemoDateTimePickerHumanFriendly.vue')['default']
|
||||
DemoDateTimePickerInline: typeof import('./src/views/demos/forms/form-elements/date-time-picker/DemoDateTimePickerInline.vue')['default']
|
||||
DemoDateTimePickerMultipleDates: typeof import('./src/views/demos/forms/form-elements/date-time-picker/DemoDateTimePickerMultipleDates.vue')['default']
|
||||
DemoDateTimePickerRange: typeof import('./src/views/demos/forms/form-elements/date-time-picker/DemoDateTimePickerRange.vue')['default']
|
||||
DemoDateTimePickerTimePicker: typeof import('./src/views/demos/forms/form-elements/date-time-picker/DemoDateTimePickerTimePicker.vue')['default']
|
||||
DemoDialogBasic: typeof import('./src/views/demos/components/dialog/DemoDialogBasic.vue')['default']
|
||||
DemoDialogForm: typeof import('./src/views/demos/components/dialog/DemoDialogForm.vue')['default']
|
||||
DemoDialogFullscreen: typeof import('./src/views/demos/components/dialog/DemoDialogFullscreen.vue')['default']
|
||||
DemoDialogLoader: typeof import('./src/views/demos/components/dialog/DemoDialogLoader.vue')['default']
|
||||
DemoDialogNesting: typeof import('./src/views/demos/components/dialog/DemoDialogNesting.vue')['default']
|
||||
DemoDialogOverflowed: typeof import('./src/views/demos/components/dialog/DemoDialogOverflowed.vue')['default']
|
||||
DemoDialogPersistent: typeof import('./src/views/demos/components/dialog/DemoDialogPersistent.vue')['default']
|
||||
DemoDialogScrollable: typeof import('./src/views/demos/components/dialog/DemoDialogScrollable.vue')['default']
|
||||
DemoEditorBasicEditor: typeof import('./src/views/demos/forms/form-elements/editor/DemoEditorBasicEditor.vue')['default']
|
||||
DemoEditorCustomEditor: typeof import('./src/views/demos/forms/form-elements/editor/DemoEditorCustomEditor.vue')['default']
|
||||
DemoExpansionPanelAccordion: typeof import('./src/views/demos/components/expansion-panel/DemoExpansionPanelAccordion.vue')['default']
|
||||
DemoExpansionPanelBasic: typeof import('./src/views/demos/components/expansion-panel/DemoExpansionPanelBasic.vue')['default']
|
||||
DemoExpansionPanelCustomIcon: typeof import('./src/views/demos/components/expansion-panel/DemoExpansionPanelCustomIcon.vue')['default']
|
||||
DemoExpansionPanelInset: typeof import('./src/views/demos/components/expansion-panel/DemoExpansionPanelInset.vue')['default']
|
||||
DemoExpansionPanelModel: typeof import('./src/views/demos/components/expansion-panel/DemoExpansionPanelModel.vue')['default']
|
||||
DemoExpansionPanelPopout: typeof import('./src/views/demos/components/expansion-panel/DemoExpansionPanelPopout.vue')['default']
|
||||
DemoExpansionPanelWithBorder: typeof import('./src/views/demos/components/expansion-panel/DemoExpansionPanelWithBorder.vue')['default']
|
||||
DemoFileInputAccept: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputAccept.vue')['default']
|
||||
DemoFileInputBasic: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputBasic.vue')['default']
|
||||
DemoFileInputChips: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputChips.vue')['default']
|
||||
DemoFileInputCounter: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputCounter.vue')['default']
|
||||
DemoFileInputDensity: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputDensity.vue')['default']
|
||||
DemoFileInputLoading: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputLoading.vue')['default']
|
||||
DemoFileInputMultiple: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputMultiple.vue')['default']
|
||||
DemoFileInputPrependIcon: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputPrependIcon.vue')['default']
|
||||
DemoFileInputSelectionSlot: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputSelectionSlot.vue')['default']
|
||||
DemoFileInputShowSize: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputShowSize.vue')['default']
|
||||
DemoFileInputValidation: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputValidation.vue')['default']
|
||||
DemoFileInputVariant: typeof import('./src/views/demos/forms/form-elements/file-input/DemoFileInputVariant.vue')['default']
|
||||
DemoFormLayoutCollapsible: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutCollapsible.vue')['default']
|
||||
DemoFormLayoutFormHint: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutFormHint.vue')['default']
|
||||
DemoFormLayoutFormSticky: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutFormSticky.vue')['default']
|
||||
DemoFormLayoutFormValidation: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutFormValidation.vue')['default']
|
||||
DemoFormLayoutFormWithTabs: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutFormWithTabs.vue')['default']
|
||||
DemoFormLayoutHorizontalForm: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutHorizontalForm.vue')['default']
|
||||
DemoFormLayoutHorizontalFormWithIcons: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutHorizontalFormWithIcons.vue')['default']
|
||||
DemoFormLayoutMultipleColumn: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutMultipleColumn.vue')['default']
|
||||
DemoFormLayoutSticky: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutSticky.vue')['default']
|
||||
DemoFormLayoutVerticalForm: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutVerticalForm.vue')['default']
|
||||
DemoFormLayoutVerticalFormWithIcons: typeof import('./src/views/demos/forms/form-layout/DemoFormLayoutVerticalFormWithIcons.vue')['default']
|
||||
DemoFormValidationSimpleFormValidation: typeof import('./src/views/demos/forms/form-validation/DemoFormValidationSimpleFormValidation.vue')['default']
|
||||
DemoFormValidationValidatingMultipleRules: typeof import('./src/views/demos/forms/form-validation/DemoFormValidationValidatingMultipleRules.vue')['default']
|
||||
DemoFormValidationValidationTypes: typeof import('./src/views/demos/forms/form-validation/DemoFormValidationValidationTypes.vue')['default']
|
||||
DemoFormWizardIconsBasic: typeof import('./src/views/demos/forms/form-wizard/form-wizard-icons/DemoFormWizardIconsBasic.vue')['default']
|
||||
DemoFormWizardIconsModernBasic: typeof import('./src/views/demos/forms/form-wizard/form-wizard-icons/DemoFormWizardIconsModernBasic.vue')['default']
|
||||
DemoFormWizardIconsModernVertical: typeof import('./src/views/demos/forms/form-wizard/form-wizard-icons/DemoFormWizardIconsModernVertical.vue')['default']
|
||||
DemoFormWizardIconsValidation: typeof import('./src/views/demos/forms/form-wizard/form-wizard-icons/DemoFormWizardIconsValidation.vue')['default']
|
||||
DemoFormWizardIconsVertical: typeof import('./src/views/demos/forms/form-wizard/form-wizard-icons/DemoFormWizardIconsVertical.vue')['default']
|
||||
DemoFormWizardNumberedBasic: typeof import('./src/views/demos/forms/form-wizard/form-wizard-numbered/DemoFormWizardNumberedBasic.vue')['default']
|
||||
DemoFormWizardNumberedModernBasic: typeof import('./src/views/demos/forms/form-wizard/form-wizard-numbered/DemoFormWizardNumberedModernBasic.vue')['default']
|
||||
DemoFormWizardNumberedModernVertical: typeof import('./src/views/demos/forms/form-wizard/form-wizard-numbered/DemoFormWizardNumberedModernVertical.vue')['default']
|
||||
DemoFormWizardNumberedValidation: typeof import('./src/views/demos/forms/form-wizard/form-wizard-numbered/DemoFormWizardNumberedValidation.vue')['default']
|
||||
DemoFormWizardNumberedVertical: typeof import('./src/views/demos/forms/form-wizard/form-wizard-numbered/DemoFormWizardNumberedVertical.vue')['default']
|
||||
DemoListActionAndItemGroup: typeof import('./src/views/demos/components/list/DemoListActionAndItemGroup.vue')['default']
|
||||
DemoListBasic: typeof import('./src/views/demos/components/list/DemoListBasic.vue')['default']
|
||||
DemoListDensity: typeof import('./src/views/demos/components/list/DemoListDensity.vue')['default']
|
||||
DemoListNav: typeof import('./src/views/demos/components/list/DemoListNav.vue')['default']
|
||||
DemoListProgressList: typeof import('./src/views/demos/components/list/DemoListProgressList.vue')['default']
|
||||
DemoListRounded: typeof import('./src/views/demos/components/list/DemoListRounded.vue')['default']
|
||||
DemoListShaped: typeof import('./src/views/demos/components/list/DemoListShaped.vue')['default']
|
||||
DemoListSubGroup: typeof import('./src/views/demos/components/list/DemoListSubGroup.vue')['default']
|
||||
DemoListThreeLine: typeof import('./src/views/demos/components/list/DemoListThreeLine.vue')['default']
|
||||
DemoListTwoLinesAndSubheader: typeof import('./src/views/demos/components/list/DemoListTwoLinesAndSubheader.vue')['default']
|
||||
DemoListUserList: typeof import('./src/views/demos/components/list/DemoListUserList.vue')['default']
|
||||
DemoMenuActivatorAndTooltip: typeof import('./src/views/demos/components/menu/DemoMenuActivatorAndTooltip.vue')['default']
|
||||
DemoMenuBasic: typeof import('./src/views/demos/components/menu/DemoMenuBasic.vue')['default']
|
||||
DemoMenuCustomTransitions: typeof import('./src/views/demos/components/menu/DemoMenuCustomTransitions.vue')['default']
|
||||
DemoMenuLocation: typeof import('./src/views/demos/components/menu/DemoMenuLocation.vue')['default']
|
||||
DemoMenuOpenOnHover: typeof import('./src/views/demos/components/menu/DemoMenuOpenOnHover.vue')['default']
|
||||
DemoMenuPopover: typeof import('./src/views/demos/components/menu/DemoMenuPopover.vue')['default']
|
||||
DemoOtpInputBasic: typeof import('./src/views/demos/forms/form-elements/otp-input/DemoOtpInputBasic.vue')['default']
|
||||
DemoOtpInputFinish: typeof import('./src/views/demos/forms/form-elements/otp-input/DemoOtpInputFinish.vue')['default']
|
||||
DemoOtpInputHidden: typeof import('./src/views/demos/forms/form-elements/otp-input/DemoOtpInputHidden.vue')['default']
|
||||
DemoPaginationBasic: typeof import('./src/views/demos/components/pagination/DemoPaginationBasic.vue')['default']
|
||||
DemoPaginationCircle: typeof import('./src/views/demos/components/pagination/DemoPaginationCircle.vue')['default']
|
||||
DemoPaginationColor: typeof import('./src/views/demos/components/pagination/DemoPaginationColor.vue')['default']
|
||||
DemoPaginationDisabled: typeof import('./src/views/demos/components/pagination/DemoPaginationDisabled.vue')['default']
|
||||
DemoPaginationIcons: typeof import('./src/views/demos/components/pagination/DemoPaginationIcons.vue')['default']
|
||||
DemoPaginationLength: typeof import('./src/views/demos/components/pagination/DemoPaginationLength.vue')['default']
|
||||
DemoPaginationOutline: typeof import('./src/views/demos/components/pagination/DemoPaginationOutline.vue')['default']
|
||||
DemoPaginationOutlineCircle: typeof import('./src/views/demos/components/pagination/DemoPaginationOutlineCircle.vue')['default']
|
||||
DemoPaginationSize: typeof import('./src/views/demos/components/pagination/DemoPaginationSize.vue')['default']
|
||||
DemoPaginationTotalVisible: typeof import('./src/views/demos/components/pagination/DemoPaginationTotalVisible.vue')['default']
|
||||
DemoProgressCircularColor: typeof import('./src/views/demos/components/progress-circular/DemoProgressCircularColor.vue')['default']
|
||||
DemoProgressCircularIndeterminate: typeof import('./src/views/demos/components/progress-circular/DemoProgressCircularIndeterminate.vue')['default']
|
||||
DemoProgressCircularRotate: typeof import('./src/views/demos/components/progress-circular/DemoProgressCircularRotate.vue')['default']
|
||||
DemoProgressCircularSize: typeof import('./src/views/demos/components/progress-circular/DemoProgressCircularSize.vue')['default']
|
||||
DemoProgressLinearBuffering: typeof import('./src/views/demos/components/progress-linear/DemoProgressLinearBuffering.vue')['default']
|
||||
DemoProgressLinearColor: typeof import('./src/views/demos/components/progress-linear/DemoProgressLinearColor.vue')['default']
|
||||
DemoProgressLinearIndeterminate: typeof import('./src/views/demos/components/progress-linear/DemoProgressLinearIndeterminate.vue')['default']
|
||||
DemoProgressLinearReversed: typeof import('./src/views/demos/components/progress-linear/DemoProgressLinearReversed.vue')['default']
|
||||
DemoProgressLinearRounded: typeof import('./src/views/demos/components/progress-linear/DemoProgressLinearRounded.vue')['default']
|
||||
DemoProgressLinearSlots: typeof import('./src/views/demos/components/progress-linear/DemoProgressLinearSlots.vue')['default']
|
||||
DemoProgressLinearStriped: typeof import('./src/views/demos/components/progress-linear/DemoProgressLinearStriped.vue')['default']
|
||||
DemoRadioBasic: typeof import('./src/views/demos/forms/form-elements/radio/DemoRadioBasic.vue')['default']
|
||||
DemoRadioColors: typeof import('./src/views/demos/forms/form-elements/radio/DemoRadioColors.vue')['default']
|
||||
DemoRadioDensity: typeof import('./src/views/demos/forms/form-elements/radio/DemoRadioDensity.vue')['default']
|
||||
DemoRadioIcon: typeof import('./src/views/demos/forms/form-elements/radio/DemoRadioIcon.vue')['default']
|
||||
DemoRadioInline: typeof import('./src/views/demos/forms/form-elements/radio/DemoRadioInline.vue')['default']
|
||||
DemoRadioLabelSlot: typeof import('./src/views/demos/forms/form-elements/radio/DemoRadioLabelSlot.vue')['default']
|
||||
DemoRadioValidation: typeof import('./src/views/demos/forms/form-elements/radio/DemoRadioValidation.vue')['default']
|
||||
DemoRangeSliderBasic: typeof import('./src/views/demos/forms/form-elements/range-slider/DemoRangeSliderBasic.vue')['default']
|
||||
DemoRangeSliderColor: typeof import('./src/views/demos/forms/form-elements/range-slider/DemoRangeSliderColor.vue')['default']
|
||||
DemoRangeSliderDisabled: typeof import('./src/views/demos/forms/form-elements/range-slider/DemoRangeSliderDisabled.vue')['default']
|
||||
DemoRangeSliderStep: typeof import('./src/views/demos/forms/form-elements/range-slider/DemoRangeSliderStep.vue')['default']
|
||||
DemoRangeSliderThumbLabel: typeof import('./src/views/demos/forms/form-elements/range-slider/DemoRangeSliderThumbLabel.vue')['default']
|
||||
DemoRangeSliderVertical: typeof import('./src/views/demos/forms/form-elements/range-slider/DemoRangeSliderVertical.vue')['default']
|
||||
DemoRatingBasic: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingBasic.vue')['default']
|
||||
DemoRatingClearable: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingClearable.vue')['default']
|
||||
DemoRatingColors: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingColors.vue')['default']
|
||||
DemoRatingDensity: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingDensity.vue')['default']
|
||||
DemoRatingHover: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingHover.vue')['default']
|
||||
DemoRatingIncremented: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingIncremented.vue')['default']
|
||||
DemoRatingItemSlot: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingItemSlot.vue')['default']
|
||||
DemoRatingLength: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingLength.vue')['default']
|
||||
DemoRatingReadonly: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingReadonly.vue')['default']
|
||||
DemoRatingSize: typeof import('./src/views/demos/forms/form-elements/rating/DemoRatingSize.vue')['default']
|
||||
DemoSelectBasic: typeof import('./src/views/demos/forms/form-elements/select/DemoSelectBasic.vue')['default']
|
||||
DemoSelectChips: typeof import('./src/views/demos/forms/form-elements/select/DemoSelectChips.vue')['default']
|
||||
DemoSelectCustomTextAndValue: typeof import('./src/views/demos/forms/form-elements/select/DemoSelectCustomTextAndValue.vue')['default']
|
||||
DemoSelectDensity: typeof import('./src/views/demos/forms/form-elements/select/DemoSelectDensity.vue')['default']
|
||||
DemoSelectIcons: typeof import('./src/views/demos/forms/form-elements/select/DemoSelectIcons.vue')['default']
|
||||
DemoSelectMenuProps: typeof import('./src/views/demos/forms/form-elements/select/DemoSelectMenuProps.vue')['default']
|
||||
DemoSelectMultiple: typeof import('./src/views/demos/forms/form-elements/select/DemoSelectMultiple.vue')['default']
|
||||
DemoSelectSelectionSlot: typeof import('./src/views/demos/forms/form-elements/select/DemoSelectSelectionSlot.vue')['default']
|
||||
DemoSelectVariant: typeof import('./src/views/demos/forms/form-elements/select/DemoSelectVariant.vue')['default']
|
||||
DemoSimpleTableBasic: typeof import('./src/views/demos/forms/tables/simple-table/DemoSimpleTableBasic.vue')['default']
|
||||
DemoSimpleTableDensity: typeof import('./src/views/demos/forms/tables/simple-table/DemoSimpleTableDensity.vue')['default']
|
||||
DemoSimpleTableFixedHeader: typeof import('./src/views/demos/forms/tables/simple-table/DemoSimpleTableFixedHeader.vue')['default']
|
||||
DemoSimpleTableHeight: typeof import('./src/views/demos/forms/tables/simple-table/DemoSimpleTableHeight.vue')['default']
|
||||
DemoSimpleTableTheme: typeof import('./src/views/demos/forms/tables/simple-table/DemoSimpleTableTheme.vue')['default']
|
||||
DemoSliderAppendAndPrepend: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderAppendAndPrepend.vue')['default']
|
||||
DemoSliderAppendTextField: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderAppendTextField.vue')['default']
|
||||
DemoSliderBasic: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderBasic.vue')['default']
|
||||
DemoSliderColors: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderColors.vue')['default']
|
||||
DemoSliderDisabledAndReadonly: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderDisabledAndReadonly.vue')['default']
|
||||
DemoSliderIcons: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderIcons.vue')['default']
|
||||
DemoSliderMinAndMax: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderMinAndMax.vue')['default']
|
||||
DemoSliderSize: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderSize.vue')['default']
|
||||
DemoSliderStep: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderStep.vue')['default']
|
||||
DemoSliderThumb: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderThumb.vue')['default']
|
||||
DemoSliderTicks: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderTicks.vue')['default']
|
||||
DemoSliderValidation: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderValidation.vue')['default']
|
||||
DemoSliderVertical: typeof import('./src/views/demos/forms/form-elements/slider/DemoSliderVertical.vue')['default']
|
||||
DemoSnackbarBasic: typeof import('./src/views/demos/components/snackbar/DemoSnackbarBasic.vue')['default']
|
||||
DemoSnackbarMultiLine: typeof import('./src/views/demos/components/snackbar/DemoSnackbarMultiLine.vue')['default']
|
||||
DemoSnackbarPosition: typeof import('./src/views/demos/components/snackbar/DemoSnackbarPosition.vue')['default']
|
||||
DemoSnackbarTimeout: typeof import('./src/views/demos/components/snackbar/DemoSnackbarTimeout.vue')['default']
|
||||
DemoSnackbarTransition: typeof import('./src/views/demos/components/snackbar/DemoSnackbarTransition.vue')['default']
|
||||
DemoSnackbarVariants: typeof import('./src/views/demos/components/snackbar/DemoSnackbarVariants.vue')['default']
|
||||
DemoSnackbarVertical: typeof import('./src/views/demos/components/snackbar/DemoSnackbarVertical.vue')['default']
|
||||
DemoSnackbarWithAction: typeof import('./src/views/demos/components/snackbar/DemoSnackbarWithAction.vue')['default']
|
||||
DemoSwiperAutoplay: typeof import('./src/views/demos/components/swiper/DemoSwiperAutoplay.vue')['default']
|
||||
DemoSwiperBasic: typeof import('./src/views/demos/components/swiper/DemoSwiperBasic.vue')['default']
|
||||
DemoSwiperCenteredSlidesOption1: typeof import('./src/views/demos/components/swiper/DemoSwiperCenteredSlidesOption1.vue')['default']
|
||||
DemoSwiperCenteredSlidesOption2: typeof import('./src/views/demos/components/swiper/DemoSwiperCenteredSlidesOption2.vue')['default']
|
||||
DemoSwiperCoverflowEffect: typeof import('./src/views/demos/components/swiper/DemoSwiperCoverflowEffect.vue')['default']
|
||||
DemoSwiperCubeEffect: typeof import('./src/views/demos/components/swiper/DemoSwiperCubeEffect.vue')['default']
|
||||
DemoSwiperFade: typeof import('./src/views/demos/components/swiper/DemoSwiperFade.vue')['default']
|
||||
DemoSwiperGallery: typeof import('./src/views/demos/components/swiper/DemoSwiperGallery.vue')['default']
|
||||
DemoSwiperGrid: typeof import('./src/views/demos/components/swiper/DemoSwiperGrid.vue')['default']
|
||||
DemoSwiperLazyLoading: typeof import('./src/views/demos/components/swiper/DemoSwiperLazyLoading.vue')['default']
|
||||
DemoSwiperMultipleSlidesPerView: typeof import('./src/views/demos/components/swiper/DemoSwiperMultipleSlidesPerView.vue')['default']
|
||||
DemoSwiperNavigation: typeof import('./src/views/demos/components/swiper/DemoSwiperNavigation.vue')['default']
|
||||
DemoSwiperPagination: typeof import('./src/views/demos/components/swiper/DemoSwiperPagination.vue')['default']
|
||||
DemoSwiperProgress: typeof import('./src/views/demos/components/swiper/DemoSwiperProgress.vue')['default']
|
||||
DemoSwiperResponsiveBreakpoints: typeof import('./src/views/demos/components/swiper/DemoSwiperResponsiveBreakpoints.vue')['default']
|
||||
DemoSwiperVirtualSlides: typeof import('./src/views/demos/components/swiper/DemoSwiperVirtualSlides.vue')['default']
|
||||
DemoSwitchBasic: typeof import('./src/views/demos/forms/form-elements/switch/DemoSwitchBasic.vue')['default']
|
||||
DemoSwitchColors: typeof import('./src/views/demos/forms/form-elements/switch/DemoSwitchColors.vue')['default']
|
||||
DemoSwitchInset: typeof import('./src/views/demos/forms/form-elements/switch/DemoSwitchInset.vue')['default']
|
||||
DemoSwitchLabelSlot: typeof import('./src/views/demos/forms/form-elements/switch/DemoSwitchLabelSlot.vue')['default']
|
||||
DemoSwitchModelAsArray: typeof import('./src/views/demos/forms/form-elements/switch/DemoSwitchModelAsArray.vue')['default']
|
||||
DemoSwitchStates: typeof import('./src/views/demos/forms/form-elements/switch/DemoSwitchStates.vue')['default']
|
||||
DemoSwitchTrueAndFalseValue: typeof import('./src/views/demos/forms/form-elements/switch/DemoSwitchTrueAndFalseValue.vue')['default']
|
||||
DemoTabsAlignment: typeof import('./src/views/demos/components/tabs/DemoTabsAlignment.vue')['default']
|
||||
DemoTabsBasic: typeof import('./src/views/demos/components/tabs/DemoTabsBasic.vue')['default']
|
||||
DemoTabsBasicPill: typeof import('./src/views/demos/components/tabs/DemoTabsBasicPill.vue')['default']
|
||||
DemoTabsCustomIcons: typeof import('./src/views/demos/components/tabs/DemoTabsCustomIcons.vue')['default']
|
||||
DemoTabsDynamic: typeof import('./src/views/demos/components/tabs/DemoTabsDynamic.vue')['default']
|
||||
DemoTabsFixed: typeof import('./src/views/demos/components/tabs/DemoTabsFixed.vue')['default']
|
||||
DemoTabsGrow: typeof import('./src/views/demos/components/tabs/DemoTabsGrow.vue')['default']
|
||||
DemoTabsPagination: typeof import('./src/views/demos/components/tabs/DemoTabsPagination.vue')['default']
|
||||
DemoTabsProgrammaticNavigation: typeof import('./src/views/demos/components/tabs/DemoTabsProgrammaticNavigation.vue')['default']
|
||||
DemoTabsStacked: typeof import('./src/views/demos/components/tabs/DemoTabsStacked.vue')['default']
|
||||
DemoTabsVertical: typeof import('./src/views/demos/components/tabs/DemoTabsVertical.vue')['default']
|
||||
DemoTabsVerticalPill: typeof import('./src/views/demos/components/tabs/DemoTabsVerticalPill.vue')['default']
|
||||
DemoTextareaAutoGrow: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaAutoGrow.vue')['default']
|
||||
DemoTextareaBasic: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaBasic.vue')['default']
|
||||
DemoTextareaBrowserAutocomplete: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaBrowserAutocomplete.vue')['default']
|
||||
DemoTextareaClearable: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaClearable.vue')['default']
|
||||
DemoTextareaCounter: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaCounter.vue')['default']
|
||||
DemoTextareaIcons: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaIcons.vue')['default']
|
||||
DemoTextareaNoResize: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaNoResize.vue')['default']
|
||||
DemoTextareaRows: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaRows.vue')['default']
|
||||
DemoTextareaStates: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaStates.vue')['default']
|
||||
DemoTextareaValidation: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaValidation.vue')['default']
|
||||
DemoTextareaVariant: typeof import('./src/views/demos/forms/form-elements/textarea/DemoTextareaVariant.vue')['default']
|
||||
DemoTextfieldBasic: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldBasic.vue')['default']
|
||||
DemoTextfieldClearable: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldClearable.vue')['default']
|
||||
DemoTextfieldCounter: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldCounter.vue')['default']
|
||||
DemoTextfieldCustomColors: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldCustomColors.vue')['default']
|
||||
DemoTextfieldDensity: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldDensity.vue')['default']
|
||||
DemoTextfieldIconEvents: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldIconEvents.vue')['default']
|
||||
DemoTextfieldIcons: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldIcons.vue')['default']
|
||||
DemoTextfieldIconSlots: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldIconSlots.vue')['default']
|
||||
DemoTextfieldLabelSlot: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldLabelSlot.vue')['default']
|
||||
DemoTextfieldPasswordInput: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldPasswordInput.vue')['default']
|
||||
DemoTextfieldPrefixesAndSuffixes: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldPrefixesAndSuffixes.vue')['default']
|
||||
DemoTextfieldSingleLine: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldSingleLine.vue')['default']
|
||||
DemoTextfieldState: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldState.vue')['default']
|
||||
DemoTextfieldValidation: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldValidation.vue')['default']
|
||||
DemoTextfieldVariant: typeof import('./src/views/demos/forms/form-elements/textfield/DemoTextfieldVariant.vue')['default']
|
||||
DemoTooltipDelayOnHover: typeof import('./src/views/demos/components/tooltip/DemoTooltipDelayOnHover.vue')['default']
|
||||
DemoTooltipEvents: typeof import('./src/views/demos/components/tooltip/DemoTooltipEvents.vue')['default']
|
||||
DemoTooltipLocation: typeof import('./src/views/demos/components/tooltip/DemoTooltipLocation.vue')['default']
|
||||
DemoTooltipTooltipOnVariousElements: typeof import('./src/views/demos/components/tooltip/DemoTooltipTooltipOnVariousElements.vue')['default']
|
||||
DemoTooltipTransition: typeof import('./src/views/demos/components/tooltip/DemoTooltipTransition.vue')['default']
|
||||
DemoTooltipVModelSupport: typeof import('./src/views/demos/components/tooltip/DemoTooltipVModelSupport.vue')['default']
|
||||
DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default']
|
||||
DropZone: typeof import('./src/@core/components/DropZone.vue')['default']
|
||||
EnableOneTimePasswordDialog: typeof import('./src/components/dialogs/EnableOneTimePasswordDialog.vue')['default']
|
||||
ErrorHeader: typeof import('./src/components/ErrorHeader.vue')['default']
|
||||
EventSwitcher: typeof import('./src/components/portal/EventSwitcher.vue')['default']
|
||||
I18n: typeof import('./src/@core/components/I18n.vue')['default']
|
||||
MoreBtn: typeof import('./src/@core/components/MoreBtn.vue')['default']
|
||||
Notifications: typeof import('./src/@core/components/Notifications.vue')['default']
|
||||
PaymentProvidersDialog: typeof import('./src/components/dialogs/PaymentProvidersDialog.vue')['default']
|
||||
PricingPlanDialog: typeof import('./src/components/dialogs/PricingPlanDialog.vue')['default']
|
||||
ProductDescriptionEditor: typeof import('./src/@core/components/ProductDescriptionEditor.vue')['default']
|
||||
ReferAndEarnDialog: typeof import('./src/components/dialogs/ReferAndEarnDialog.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScrollToTop: typeof import('./src/@core/components/ScrollToTop.vue')['default']
|
||||
ShareProjectDialog: typeof import('./src/components/dialogs/ShareProjectDialog.vue')['default']
|
||||
Shortcuts: typeof import('./src/@core/components/Shortcuts.vue')['default']
|
||||
StatusCard: typeof import('./src/components/portal/StatusCard.vue')['default']
|
||||
TablePagination: typeof import('./src/@core/components/TablePagination.vue')['default']
|
||||
TheCustomizer: typeof import('./src/@core/components/TheCustomizer.vue')['default']
|
||||
ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default']
|
||||
TimelineBasic: typeof import('./src/views/demos/components/timeline/TimelineBasic.vue')['default']
|
||||
TimelineOutlined: typeof import('./src/views/demos/components/timeline/TimelineOutlined.vue')['default']
|
||||
TimelineWithIcons: typeof import('./src/views/demos/components/timeline/TimelineWithIcons.vue')['default']
|
||||
TiptapEditor: typeof import('./src/@core/components/TiptapEditor.vue')['default']
|
||||
TwoFactorAuthDialog: typeof import('./src/components/dialogs/TwoFactorAuthDialog.vue')['default']
|
||||
UserInfoEditDialog: typeof import('./src/components/dialogs/UserInfoEditDialog.vue')['default']
|
||||
UserUpgradePlanDialog: typeof import('./src/components/dialogs/UserUpgradePlanDialog.vue')['default']
|
||||
VueApexCharts: typeof import('vue3-apexcharts')['default']
|
||||
}
|
||||
}
|
||||
|
||||
123
apps/portal/src/components/portal/EventSwitcher.vue
Normal file
123
apps/portal/src/components/portal/EventSwitcher.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { usePortalStore } from '@/stores/usePortalStore'
|
||||
|
||||
const portal = usePortalStore()
|
||||
|
||||
const menuOpen = ref(false)
|
||||
|
||||
function statusColor(status: string): string {
|
||||
if (status === 'approved') return 'success'
|
||||
if (status === 'pending' || status === 'applied' || status === 'invited') return 'warning'
|
||||
if (status === 'rejected') return 'error'
|
||||
|
||||
return 'secondary'
|
||||
}
|
||||
|
||||
function statusLabel(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'In behandeling',
|
||||
applied: 'In behandeling',
|
||||
invited: 'Uitgenodigd',
|
||||
approved: 'Goedgekeurd',
|
||||
rejected: 'Afgewezen',
|
||||
no_show: 'Niet verschenen',
|
||||
}
|
||||
|
||||
return map[status] ?? status
|
||||
}
|
||||
|
||||
function selectEvent(id: string) {
|
||||
portal.setActiveEvent(id)
|
||||
menuOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="portal.userEvents.length === 0"
|
||||
class="text-body-2 text-medium-emphasis ms-2 ms-sm-4 d-flex align-center min-w-0"
|
||||
>
|
||||
Geen evenement
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="portal.userEvents.length === 1 && portal.activeEvent"
|
||||
class="ms-2 ms-sm-4 d-flex align-center gap-2 min-w-0 flex-grow-1 flex-sm-grow-0"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-calendar-event"
|
||||
size="20"
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<span class="text-body-1 font-weight-medium text-truncate">{{ portal.activeEvent.event_name }}</span>
|
||||
<VChip
|
||||
:color="statusColor(portal.activeEvent.person_status)"
|
||||
size="small"
|
||||
label
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
{{ statusLabel(portal.activeEvent.person_status) }}
|
||||
</VChip>
|
||||
</div>
|
||||
|
||||
<VMenu
|
||||
v-else
|
||||
v-model="menuOpen"
|
||||
location="bottom"
|
||||
:close-on-content-click="true"
|
||||
>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<VBtn
|
||||
v-bind="menuProps"
|
||||
variant="text"
|
||||
class="ms-2 ms-sm-4 text-none min-w-0"
|
||||
rounded="lg"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-calendar-event"
|
||||
start
|
||||
size="20"
|
||||
/>
|
||||
<span class="text-truncate max-w-[200px] sm:max-w-[280px]">{{ portal.activeEvent?.event_name ?? 'Kies evenement' }}</span>
|
||||
<VIcon
|
||||
icon="tabler-chevron-down"
|
||||
end
|
||||
size="18"
|
||||
/>
|
||||
</VBtn>
|
||||
</template>
|
||||
<VList
|
||||
density="compact"
|
||||
min-width="280"
|
||||
>
|
||||
<VListSubheader class="text-caption">
|
||||
Jouw evenementen
|
||||
</VListSubheader>
|
||||
<VListItem
|
||||
v-for="ev in portal.userEvents"
|
||||
:key="ev.event_id"
|
||||
:active="ev.event_id === portal.activeEventId"
|
||||
@click="selectEvent(ev.event_id)"
|
||||
>
|
||||
<VListItemTitle class="text-wrap">
|
||||
{{ ev.event_name }}
|
||||
</VListItemTitle>
|
||||
<VListItemSubtitle class="d-flex flex-column gap-1 mt-1">
|
||||
<div class="d-flex align-center gap-2 flex-wrap">
|
||||
<VChip
|
||||
:color="statusColor(ev.person_status)"
|
||||
size="x-small"
|
||||
label
|
||||
>
|
||||
{{ statusLabel(ev.person_status) }}
|
||||
</VChip>
|
||||
</div>
|
||||
<span
|
||||
v-if="ev.organisation_name"
|
||||
class="text-caption text-medium-emphasis"
|
||||
>{{ ev.organisation_name }}</span>
|
||||
</VListItemSubtitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VMenu>
|
||||
</template>
|
||||
161
apps/portal/src/components/portal/StatusCard.vue
Normal file
161
apps/portal/src/components/portal/StatusCard.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
variant: 'pending' | 'approved' | 'rejected'
|
||||
eventName: string
|
||||
registeredAt?: string | null
|
||||
nextShiftSummary?: string | null
|
||||
}>()
|
||||
|
||||
const registeredLabel = computed(() => {
|
||||
if (!props.registeredAt) return null
|
||||
try {
|
||||
return new Date(props.registeredAt).toLocaleDateString('nl-NL', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard
|
||||
variant="tonal"
|
||||
:color="variant === 'approved' ? 'success' : variant === 'pending' ? 'warning' : 'error'"
|
||||
class="pa-6"
|
||||
>
|
||||
<template v-if="variant === 'pending'">
|
||||
<div class="d-flex align-start gap-3">
|
||||
<VIcon
|
||||
icon="tabler-clock"
|
||||
size="32"
|
||||
/>
|
||||
<div>
|
||||
<h5 class="text-h5 mb-2">
|
||||
Je registratie wordt beoordeeld
|
||||
</h5>
|
||||
<p class="text-body-1 mb-2">
|
||||
Je hebt je aangemeld voor <strong>{{ eventName }}</strong>.
|
||||
De organisatie beoordeelt je registratie.
|
||||
</p>
|
||||
<p class="text-body-1 mb-2">
|
||||
Je ontvangt een e-mail zodra er een besluit is.
|
||||
</p>
|
||||
<p
|
||||
v-if="registeredLabel"
|
||||
class="text-body-2 text-medium-emphasis mb-0"
|
||||
>
|
||||
Aangemeld op: {{ registeredLabel }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="variant === 'rejected'">
|
||||
<div class="d-flex align-start gap-3">
|
||||
<VIcon
|
||||
icon="tabler-circle-x"
|
||||
size="32"
|
||||
/>
|
||||
<div>
|
||||
<h5 class="text-h5 mb-2">
|
||||
Je aanmelding is niet geselecteerd
|
||||
</h5>
|
||||
<p class="text-body-1 mb-0">
|
||||
Helaas is je aanmelding voor <strong>{{ eventName }}</strong> niet geselecteerd.
|
||||
Neem contact op met de organisatie als je vragen hebt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="d-flex align-start gap-3 mb-6">
|
||||
<VIcon
|
||||
icon="tabler-circle-check"
|
||||
size="32"
|
||||
/>
|
||||
<div>
|
||||
<h5 class="text-h5 mb-1">
|
||||
Welkom bij {{ eventName }}!
|
||||
</h5>
|
||||
<p class="text-body-2 text-medium-emphasis mb-0">
|
||||
Je bent goedgekeurd als vrijwilliger.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VRow class="mb-6">
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VCard
|
||||
:to="{ name: 'portal-shifts' }"
|
||||
variant="outlined"
|
||||
class="pa-4 h-100 text-decoration-none"
|
||||
>
|
||||
<div class="text-subtitle-2 text-medium-emphasis mb-1">
|
||||
Mijn Shifts
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
Rooster bekijken
|
||||
</div>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VCard
|
||||
variant="outlined"
|
||||
class="pa-4 h-100 text-medium-emphasis"
|
||||
>
|
||||
<div class="text-subtitle-2 mb-1">
|
||||
Shifts claimen
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
Binnenkort beschikbaar
|
||||
</div>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol
|
||||
cols="12"
|
||||
sm="4"
|
||||
>
|
||||
<VCard
|
||||
:to="{ name: 'portal-profile' }"
|
||||
variant="outlined"
|
||||
class="pa-4 h-100 text-decoration-none"
|
||||
>
|
||||
<div class="text-subtitle-2 text-medium-emphasis mb-1">
|
||||
Profiel
|
||||
</div>
|
||||
<div class="text-body-2">
|
||||
Gegevens bekijken
|
||||
</div>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
|
||||
<div class="text-subtitle-1 font-weight-bold mb-2">
|
||||
Komende shift
|
||||
</div>
|
||||
<p
|
||||
v-if="nextShiftSummary"
|
||||
class="text-body-1 mb-0"
|
||||
>
|
||||
{{ nextShiftSummary }}
|
||||
</p>
|
||||
<p
|
||||
v-else
|
||||
class="text-body-2 text-medium-emphasis mb-0"
|
||||
>
|
||||
Er is nog geen shift ingepland. Je coördinator houdt je op de hoogte.
|
||||
</p>
|
||||
</template>
|
||||
</VCard>
|
||||
</template>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import EventSwitcher from '@/components/portal/EventSwitcher.vue'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
|
||||
const { injectSkinClasses } = useSkins()
|
||||
@@ -6,6 +7,7 @@ const { injectSkinClasses } = useSkins()
|
||||
injectSkinClasses()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
@@ -29,6 +31,16 @@ watch([isFallbackStateActive, refLoadingIndicator], () => {
|
||||
if (!isFallbackStateActive.value && refLoadingIndicator.value)
|
||||
refLoadingIndicator.value.resolveHandle()
|
||||
}, { immediate: true })
|
||||
|
||||
async function logoutAndRedirect(): Promise<void> {
|
||||
await authStore.logout()
|
||||
await router.push('/login')
|
||||
}
|
||||
|
||||
async function logoutFromDrawer(): Promise<void> {
|
||||
isMobileMenuOpen.value = false
|
||||
await logoutAndRedirect()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -49,7 +61,7 @@ watch([isFallbackStateActive, refLoadingIndicator], () => {
|
||||
<!-- Logo & Brand -->
|
||||
<RouterLink
|
||||
to="/"
|
||||
class="d-flex align-center gap-x-2 text-decoration-none"
|
||||
class="d-flex align-center gap-x-2 text-decoration-none flex-shrink-0"
|
||||
>
|
||||
<VIcon
|
||||
icon="tabler-users-group"
|
||||
@@ -61,6 +73,11 @@ watch([isFallbackStateActive, refLoadingIndicator], () => {
|
||||
</span>
|
||||
</RouterLink>
|
||||
|
||||
<EventSwitcher
|
||||
v-if="authStore.isAuthenticated"
|
||||
class="min-w-0 flex-grow-1 flex-sm-grow-0"
|
||||
/>
|
||||
|
||||
<!-- Mobile nav toggle -->
|
||||
<VAppBarNavIcon
|
||||
class="d-sm-none ms-auto"
|
||||
@@ -95,7 +112,7 @@ watch([isFallbackStateActive, refLoadingIndicator], () => {
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="small"
|
||||
@click="authStore.logout(); $router.push('/login')"
|
||||
@click="logoutAndRedirect"
|
||||
>
|
||||
<VIcon
|
||||
start
|
||||
@@ -144,7 +161,7 @@ watch([isFallbackStateActive, refLoadingIndicator], () => {
|
||||
v-if="authStore.isAuthenticated"
|
||||
prepend-icon="tabler-logout"
|
||||
title="Uitloggen"
|
||||
@click="authStore.logout(); $router.push('/login'); isMobileMenuOpen = false"
|
||||
@click="logoutFromDrawer"
|
||||
/>
|
||||
|
||||
<VListItem
|
||||
|
||||
@@ -49,10 +49,14 @@ apiClient.interceptors.response.use(
|
||||
const authStore = useAuthStore()
|
||||
|
||||
if (authStore.isInitialized) {
|
||||
authStore.logout()
|
||||
authStore.clearLocalSession()
|
||||
|
||||
if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
|
||||
window.location.href = '/login'
|
||||
if (typeof window !== 'undefined') {
|
||||
const path = window.location.pathname
|
||||
const publicPaths = ['/login', '/wachtwoord-vergeten', '/wachtwoord-resetten']
|
||||
if (!publicPaths.some(p => path.startsWith(p)) && !path.startsWith('/register')) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import StatusCard from '@/components/portal/StatusCard.vue'
|
||||
import { usePortalStore } from '@/stores/usePortalStore'
|
||||
import type { PortalPersonPayload } from '@/types/portal'
|
||||
|
||||
definePage({
|
||||
name: 'portal-dashboard',
|
||||
meta: {
|
||||
@@ -6,23 +10,124 @@ definePage({
|
||||
requiresAuth: true,
|
||||
},
|
||||
})
|
||||
|
||||
const portal = usePortalStore()
|
||||
|
||||
const effectiveStatus = computed(() => {
|
||||
const fromPerson = portal.currentPerson?.status
|
||||
if (fromPerson) return fromPerson
|
||||
|
||||
return portal.activeEvent?.person_status ?? 'pending'
|
||||
})
|
||||
|
||||
const statusVariant = computed((): 'pending' | 'approved' | 'rejected' => {
|
||||
const s = effectiveStatus.value
|
||||
if (s === 'approved') return 'approved'
|
||||
if (s === 'rejected') return 'rejected'
|
||||
|
||||
return 'pending'
|
||||
})
|
||||
|
||||
const eventTitle = computed(() => portal.activeEvent?.event_name ?? 'dit evenement')
|
||||
|
||||
const registeredAt = computed(() => portal.currentPerson?.created_at ?? null)
|
||||
|
||||
function formatNextShift(person: PortalPersonPayload | null): string | null {
|
||||
const list = person?.shift_assignments
|
||||
if (!list?.length) return null
|
||||
|
||||
const usable = list.filter(
|
||||
a => a.shift?.time_slot?.date && (a.status === 'approved' || a.status === 'pending_approval'),
|
||||
)
|
||||
if (!usable.length) return null
|
||||
|
||||
usable.sort((a, b) => {
|
||||
const da = a.shift?.time_slot?.date ?? ''
|
||||
const db = b.shift?.time_slot?.date ?? ''
|
||||
|
||||
return da.localeCompare(db)
|
||||
})
|
||||
|
||||
const a = usable[0]!
|
||||
const slot = a.shift?.time_slot
|
||||
const section = a.shift?.festival_section?.name
|
||||
if (!slot?.date) return null
|
||||
|
||||
const dateStr = new Date(`${slot.date}T12:00:00`).toLocaleDateString('nl-NL', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
})
|
||||
const start = slot.start_time?.slice(0, 5) ?? ''
|
||||
const end = slot.end_time?.slice(0, 5) ?? ''
|
||||
const timePart = start && end ? `${start} – ${end}` : start || ''
|
||||
|
||||
const place = section ? ` — ${section}` : ''
|
||||
|
||||
return `📅 ${dateStr}${timePart ? `, ${timePart}` : ''}${place}`
|
||||
}
|
||||
|
||||
const nextShiftSummary = computed(() => formatNextShift(portal.currentPerson))
|
||||
|
||||
onMounted(async () => {
|
||||
await portal.hydrateAfterAuth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VRow justify="center">
|
||||
<VCol
|
||||
cols="12"
|
||||
md="8"
|
||||
lg="6"
|
||||
lg="10"
|
||||
>
|
||||
<VCard class="text-center pa-6">
|
||||
<VCardTitle class="text-h5">
|
||||
Mijn Dashboard
|
||||
</VCardTitle>
|
||||
<VCardSubtitle>
|
||||
Welkom terug! Hier zie je je shifts en informatie.
|
||||
</VCardSubtitle>
|
||||
</VCard>
|
||||
<VSkeletonLoader
|
||||
v-if="portal.isLoadingEvents"
|
||||
type="article"
|
||||
/>
|
||||
|
||||
<VAlert
|
||||
v-else-if="portal.loadError"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ portal.loadError }}
|
||||
</VAlert>
|
||||
|
||||
<VAlert
|
||||
v-else-if="!portal.userEvents.length"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
Je hebt nog geen evenementen waarvoor je bent aangemeld, of ze zijn niet gekoppeld aan dit account.
|
||||
Meld je aan via de link van je organisatie, of log in met hetzelfde e-mailadres als bij je aanmelding.
|
||||
</VAlert>
|
||||
|
||||
<template v-else>
|
||||
<VSkeletonLoader
|
||||
v-if="portal.isLoadingPerson && !portal.currentPerson"
|
||||
type="article"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<VAlert
|
||||
v-else-if="!portal.currentPerson && !portal.isLoadingPerson"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
We konden je registratie voor dit evenement niet ophalen. Controleer of je met het juiste account bent ingelogd,
|
||||
of probeer het later opnieuw.
|
||||
</VAlert>
|
||||
|
||||
<StatusCard
|
||||
:variant="statusVariant"
|
||||
:event-name="eventTitle"
|
||||
:registered-at="registeredAt"
|
||||
:next-shift-summary="nextShiftSummary"
|
||||
/>
|
||||
</template>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,8 @@ import authV2LoginIllustrationBorderedLight from '@images/pages/auth-v2-login-il
|
||||
import authV2LoginIllustrationBorderedDark from '@images/pages/auth-v2-login-illustration-bordered-dark.png'
|
||||
import miscMaskLight from '@images/pages/misc-mask-light.png'
|
||||
import miscMaskDark from '@images/pages/misc-mask-dark.png'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { usePortalStore } from '@/stores/usePortalStore'
|
||||
|
||||
definePage({
|
||||
name: 'login',
|
||||
@@ -15,12 +17,21 @@ definePage({
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const portalStore = usePortalStore()
|
||||
|
||||
const form = ref({
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const isPasswordVisible = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const passwordResetDone = computed(() => route.query.reset === '1')
|
||||
|
||||
const authThemeImg = useGenerateImageVariant(
|
||||
authV2LoginIllustrationLight,
|
||||
@@ -31,6 +42,37 @@ const authThemeImg = useGenerateImageVariant(
|
||||
)
|
||||
|
||||
const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
|
||||
|
||||
function mapLoginErrorMessage(message: string | undefined): string {
|
||||
if (!message) return 'Inloggen mislukt. Controleer je gegevens.'
|
||||
if (message === 'Invalid credentials' || message.toLowerCase().includes('invalid credentials'))
|
||||
return 'Ongeldig e-mailadres of wachtwoord.'
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
errorMessage.value = ''
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
await authStore.login(form.email.trim(), form.password)
|
||||
await portalStore.hydrateAfterAuth()
|
||||
const redirect = typeof route.query.to === 'string' ? route.query.to : '/dashboard'
|
||||
await router.replace(redirect || '/dashboard')
|
||||
}
|
||||
catch (error: unknown) {
|
||||
if (error instanceof Error && error.message === 'Sessie kon niet worden gestart.') {
|
||||
errorMessage.value = 'Je sessie kon niet worden geladen. Probeer het opnieuw.'
|
||||
|
||||
return
|
||||
}
|
||||
const ax = error as { response?: { data?: { message?: string } } }
|
||||
errorMessage.value = mapLoginErrorMessage(ax.response?.data?.message)
|
||||
}
|
||||
finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -100,7 +142,27 @@ const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
|
||||
</VCardText>
|
||||
|
||||
<VCardText>
|
||||
<VForm @submit.prevent>
|
||||
<VAlert
|
||||
v-if="passwordResetDone"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
density="comfortable"
|
||||
>
|
||||
Wachtwoord gewijzigd. Je kunt nu inloggen.
|
||||
</VAlert>
|
||||
|
||||
<VAlert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
density="comfortable"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</VAlert>
|
||||
|
||||
<VForm @submit.prevent="onSubmit">
|
||||
<VRow>
|
||||
<VCol cols="12">
|
||||
<VTextField
|
||||
@@ -109,6 +171,11 @@ const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
|
||||
label="E-mailadres"
|
||||
type="email"
|
||||
placeholder="je@email.nl"
|
||||
autocomplete="email"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
required
|
||||
/>
|
||||
</VCol>
|
||||
|
||||
@@ -117,30 +184,47 @@ const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
|
||||
v-model="form.password"
|
||||
label="Wachtwoord"
|
||||
placeholder="Je wachtwoord"
|
||||
autocomplete="current-password"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
:type="isPasswordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="isPasswordVisible ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
required
|
||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
||||
/>
|
||||
|
||||
<div class="d-flex align-center flex-wrap justify-end my-6">
|
||||
<a
|
||||
<div class="d-flex align-center flex-wrap justify-end my-4">
|
||||
<RouterLink
|
||||
to="/wachtwoord-vergeten"
|
||||
class="text-primary text-body-2"
|
||||
href="javascript:void(0)"
|
||||
>
|
||||
Wachtwoord vergeten?
|
||||
</a>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<VBtn
|
||||
block
|
||||
type="submit"
|
||||
color="primary"
|
||||
:loading="isSubmitting"
|
||||
>
|
||||
Inloggen
|
||||
</VBtn>
|
||||
</VCol>
|
||||
</VRow>
|
||||
</VForm>
|
||||
|
||||
<p class="text-body-2 text-center text-medium-emphasis mt-6 mb-0">
|
||||
Nog geen account?
|
||||
<RouterLink
|
||||
to="/registreren"
|
||||
class="text-primary font-weight-medium"
|
||||
>
|
||||
Meld je aan als vrijwilliger
|
||||
</RouterLink>
|
||||
→
|
||||
</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
import { usePortalStore } from '@/stores/usePortalStore'
|
||||
import { useRegistrationData, useSubmitRegistration } from '@/composables/api/useVolunteerRegistration'
|
||||
import { fullRegistrationSchema } from '@/schemas/registrationSchema'
|
||||
import type {
|
||||
@@ -28,6 +29,7 @@ definePage({
|
||||
const route = useRoute('volunteer-register')
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const portalStore = usePortalStore()
|
||||
const { mdAndUp } = useDisplay()
|
||||
|
||||
const eventSlug = computed(() => route.params.eventSlug as string)
|
||||
@@ -624,6 +626,17 @@ async function onSubmit() {
|
||||
form: payload,
|
||||
})
|
||||
|
||||
const ev = registrationData.value.event
|
||||
portalStore.savePendingEventFromRegistration({
|
||||
event_id: ev.id,
|
||||
event_name: ev.name,
|
||||
organisation_name: '',
|
||||
organisation_id: ev.organisation_id,
|
||||
person_status: 'pending',
|
||||
start_date: ev.start_date,
|
||||
end_date: ev.end_date,
|
||||
})
|
||||
|
||||
router.push({
|
||||
path: '/register/success',
|
||||
query: {
|
||||
|
||||
46
apps/portal/src/pages/registreren/index.vue
Normal file
46
apps/portal/src/pages/registreren/index.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'volunteer-register-info',
|
||||
meta: {
|
||||
layout: 'blank',
|
||||
requiresAuth: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VContainer
|
||||
class="py-12"
|
||||
style="max-inline-size: 640px;"
|
||||
>
|
||||
<VCard
|
||||
class="pa-6 pa-sm-8"
|
||||
variant="flat"
|
||||
>
|
||||
<h1 class="text-h4 mb-4">
|
||||
Aanmelden als vrijwilliger
|
||||
</h1>
|
||||
<p class="text-body-1 text-medium-emphasis mb-4">
|
||||
Vrijwilligers melden zich aan via een persoonlijke link die door de organisatie wordt gedeeld
|
||||
(bijvoorbeeld op de website van het evenement of in een uitnodiging per e-mail).
|
||||
</p>
|
||||
<p class="text-body-1 text-medium-emphasis mb-6">
|
||||
Heb je al een Crewli-account? Log dan in om je aanmeldingen te volgen.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<VBtn
|
||||
color="primary"
|
||||
to="/login"
|
||||
>
|
||||
Inloggen
|
||||
</VBtn>
|
||||
<VBtn
|
||||
variant="tonal"
|
||||
to="/"
|
||||
>
|
||||
Startpagina
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCard>
|
||||
</VContainer>
|
||||
</template>
|
||||
139
apps/portal/src/pages/wachtwoord-resetten.vue
Normal file
139
apps/portal/src/pages/wachtwoord-resetten.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { apiClient } from '@/lib/axios'
|
||||
|
||||
definePage({
|
||||
name: 'reset-password',
|
||||
meta: {
|
||||
layout: 'blank',
|
||||
requiresAuth: false,
|
||||
},
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const email = ref(typeof route.query.email === 'string' ? route.query.email : '')
|
||||
const token = ref(typeof route.query.token === 'string' ? route.query.token : '')
|
||||
const password = ref('')
|
||||
const passwordConfirmation = ref('')
|
||||
const showPassword = ref(false)
|
||||
const showPasswordConfirmation = ref(false)
|
||||
|
||||
const errorMessage = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
errorMessage.value = ''
|
||||
if (!token.value || !email.value) {
|
||||
errorMessage.value = 'Ongeldige resetlink. Vraag een nieuwe link aan.'
|
||||
|
||||
return
|
||||
}
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
await apiClient.post('/auth/reset-password', {
|
||||
email: email.value.trim(),
|
||||
password: password.value,
|
||||
password_confirmation: passwordConfirmation.value,
|
||||
token: token.value,
|
||||
})
|
||||
await router.replace({ path: '/login', query: { reset: '1' } })
|
||||
}
|
||||
catch (error: unknown) {
|
||||
const ax = error as { response?: { status?: number; data?: { message?: string } } }
|
||||
if (ax.response?.status === 404 || ax.response?.status === 422)
|
||||
errorMessage.value = ax.response?.data?.message ?? 'Resetlink ongeldig of verlopen. Vraag een nieuwe link aan.'
|
||||
else
|
||||
errorMessage.value = 'Er ging iets mis. Probeer het later opnieuw.'
|
||||
}
|
||||
finally {
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-wrapper d-flex align-center justify-center pa-4 bg-surface">
|
||||
<VCard
|
||||
flat
|
||||
:max-width="480"
|
||||
width="100%"
|
||||
class="pa-6"
|
||||
>
|
||||
<VCardTitle class="text-h5 px-0 pt-0">
|
||||
Nieuw wachtwoord
|
||||
</VCardTitle>
|
||||
<VCardSubtitle class="px-0">
|
||||
Kies een nieuw wachtwoord voor je account.
|
||||
</VCardSubtitle>
|
||||
|
||||
<VCardText class="px-0">
|
||||
<VAlert
|
||||
v-if="errorMessage"
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</VAlert>
|
||||
|
||||
<VForm @submit.prevent="onSubmit">
|
||||
<VTextField
|
||||
v-model="email"
|
||||
label="E-mailadres"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-3"
|
||||
autocomplete="email"
|
||||
hide-details="auto"
|
||||
required
|
||||
/>
|
||||
<VTextField
|
||||
v-model="password"
|
||||
label="Nieuw wachtwoord"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-3"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-inner-icon="showPassword ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
hide-details="auto"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
<VTextField
|
||||
v-model="passwordConfirmation"
|
||||
label="Bevestig wachtwoord"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-4"
|
||||
:type="showPasswordConfirmation ? 'text' : 'password'"
|
||||
:append-inner-icon="showPasswordConfirmation ? 'tabler-eye-off' : 'tabler-eye'"
|
||||
hide-details="auto"
|
||||
autocomplete="new-password"
|
||||
required
|
||||
@click:append-inner="showPasswordConfirmation = !showPasswordConfirmation"
|
||||
/>
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
block
|
||||
:loading="isSubmitting"
|
||||
>
|
||||
Wachtwoord opslaan
|
||||
</VBtn>
|
||||
</VForm>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<RouterLink
|
||||
to="/login"
|
||||
class="text-body-2 text-primary"
|
||||
>
|
||||
Terug naar inloggen
|
||||
</RouterLink>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
92
apps/portal/src/pages/wachtwoord-vergeten.vue
Normal file
92
apps/portal/src/pages/wachtwoord-vergeten.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import { apiClient } from '@/lib/axios'
|
||||
|
||||
definePage({
|
||||
name: 'forgot-password',
|
||||
meta: {
|
||||
layout: 'blank',
|
||||
requiresAuth: false,
|
||||
},
|
||||
})
|
||||
|
||||
const email = ref('')
|
||||
const isSubmitting = ref(false)
|
||||
const done = ref(false)
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
await apiClient.post('/auth/forgot-password', { email: email.value.trim() })
|
||||
}
|
||||
catch {
|
||||
// Endpoint may not exist yet — still show generic success (no email enumeration)
|
||||
}
|
||||
finally {
|
||||
isSubmitting.value = false
|
||||
done.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="auth-wrapper d-flex align-center justify-center pa-4 bg-surface">
|
||||
<VCard
|
||||
flat
|
||||
:max-width="480"
|
||||
width="100%"
|
||||
class="pa-6"
|
||||
>
|
||||
<VCardTitle class="text-h5 px-0 pt-0">
|
||||
Wachtwoord vergeten
|
||||
</VCardTitle>
|
||||
<VCardSubtitle class="px-0 text-wrap">
|
||||
Vul je e-mailadres in. Als dit adres bij ons bekend is, ontvang je een link om je wachtwoord te resetten.
|
||||
</VCardSubtitle>
|
||||
|
||||
<VCardText class="px-0">
|
||||
<VAlert
|
||||
v-if="done"
|
||||
type="success"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
Als dit e-mailadres bij ons bekend is, ontvang je een link om je wachtwoord te resetten.
|
||||
</VAlert>
|
||||
|
||||
<VForm
|
||||
v-else
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<VTextField
|
||||
v-model="email"
|
||||
label="E-mailadres"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-4"
|
||||
autocomplete="email"
|
||||
hide-details="auto"
|
||||
required
|
||||
/>
|
||||
<VBtn
|
||||
type="submit"
|
||||
color="primary"
|
||||
block
|
||||
:loading="isSubmitting"
|
||||
>
|
||||
Versturen
|
||||
</VBtn>
|
||||
</VForm>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<RouterLink
|
||||
to="/login"
|
||||
class="text-body-2 text-primary"
|
||||
>
|
||||
Terug naar inloggen
|
||||
</RouterLink>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Router } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/useAuthStore'
|
||||
|
||||
const guestOnlyPaths = ['/login', '/wachtwoord-vergeten', '/wachtwoord-resetten']
|
||||
|
||||
export function setupGuards(router: Router) {
|
||||
router.beforeEach(async (to) => {
|
||||
const authStore = useAuthStore()
|
||||
@@ -13,10 +15,10 @@ export function setupGuards(router: Router) {
|
||||
|
||||
// Public routes — no auth check needed
|
||||
if (!requiresAuth) {
|
||||
// Redirect authenticated users away from login
|
||||
if (authStore.isAuthenticated && to.path === '/login') {
|
||||
if (authStore.isAuthenticated && guestOnlyPaths.some(p => to.path === p || to.path.startsWith(`${p}/`))) {
|
||||
return { path: '/dashboard' }
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,61 +1,106 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
|
||||
interface PortalUser {
|
||||
id: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
full_name: string
|
||||
email: string
|
||||
}
|
||||
import type { AuthMeUser } from '@/types/portal'
|
||||
|
||||
const TOKEN_KEY = 'crewli_portal_token'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref<string | null>(localStorage.getItem(TOKEN_KEY))
|
||||
const user = ref<PortalUser | null>(null)
|
||||
const user = ref<AuthMeUser | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
|
||||
function setToken(newToken: string) {
|
||||
function setToken(newToken: string | null) {
|
||||
token.value = newToken
|
||||
localStorage.setItem(TOKEN_KEY, newToken)
|
||||
if (newToken)
|
||||
localStorage.setItem(TOKEN_KEY, newToken)
|
||||
else
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
function setUser(data: PortalUser) {
|
||||
function setUser(data: AuthMeUser | null) {
|
||||
user.value = data
|
||||
}
|
||||
|
||||
function logout() {
|
||||
token.value = null
|
||||
user.value = null
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
async function resetPortalStoresSync(): Promise<void> {
|
||||
const { usePortalStore } = await import('@/stores/usePortalStore')
|
||||
usePortalStore().reset()
|
||||
}
|
||||
|
||||
async function fetchUser(): Promise<boolean> {
|
||||
if (!token.value) {
|
||||
setUser(null)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await apiClient.get<{ success: boolean; data: AuthMeUser }>('/auth/me')
|
||||
setUser(data.data)
|
||||
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
setToken(null)
|
||||
setUser(null)
|
||||
await resetPortalStoresSync()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function login(email: string, password: string): Promise<void> {
|
||||
const { data } = await apiClient.post<{
|
||||
success: boolean
|
||||
data: { user: AuthMeUser; token: string }
|
||||
}>('/auth/login', { email, password })
|
||||
|
||||
setToken(data.data.token)
|
||||
setUser(data.data.user)
|
||||
const ok = await fetchUser()
|
||||
if (!ok) throw new Error('Sessie kon niet worden gestart.')
|
||||
}
|
||||
|
||||
function clearLocalSession(): void {
|
||||
setToken(null)
|
||||
setUser(null)
|
||||
void resetPortalStoresSync()
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
try {
|
||||
if (token.value)
|
||||
await apiClient.post('/auth/logout')
|
||||
}
|
||||
catch {
|
||||
// Ignore network errors; still clear local session
|
||||
}
|
||||
setToken(null)
|
||||
setUser(null)
|
||||
await resetPortalStoresSync()
|
||||
}
|
||||
|
||||
let initializePromise: Promise<void> | null = null
|
||||
|
||||
function initialize(): Promise<void> {
|
||||
if (isInitialized.value) return Promise.resolve()
|
||||
if (!initializePromise) {
|
||||
if (!initializePromise)
|
||||
initializePromise = doInitialize()
|
||||
}
|
||||
|
||||
return initializePromise
|
||||
}
|
||||
|
||||
async function doInitialize(): Promise<void> {
|
||||
if (!token.value) {
|
||||
isInitialized.value = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await apiClient.get<{ success: boolean; data: PortalUser }>('/auth/me')
|
||||
setUser(data.data)
|
||||
}
|
||||
catch {
|
||||
logout()
|
||||
await fetchUser()
|
||||
}
|
||||
finally {
|
||||
isInitialized.value = true
|
||||
@@ -69,7 +114,10 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
isInitialized,
|
||||
setToken,
|
||||
setUser,
|
||||
login,
|
||||
logout,
|
||||
fetchUser,
|
||||
initialize,
|
||||
clearLocalSession,
|
||||
}
|
||||
})
|
||||
|
||||
203
apps/portal/src/stores/usePortalStore.ts
Normal file
203
apps/portal/src/stores/usePortalStore.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { apiClient } from '@/lib/axios'
|
||||
import type { AuthMeUser, PortalEvent, PortalPersonPayload } from '@/types/portal'
|
||||
|
||||
const STORAGE_EVENTS = 'crewli_portal_user_events_v1'
|
||||
const STORAGE_ACTIVE_EVENT = 'crewli_portal_active_event_id_v1'
|
||||
|
||||
function readStoredEvents(): PortalEvent[] {
|
||||
if (typeof localStorage === 'undefined') return []
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_EVENTS)
|
||||
if (!raw) return []
|
||||
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
if (!Array.isArray(parsed)) return []
|
||||
|
||||
return parsed.filter(
|
||||
(e): e is PortalEvent =>
|
||||
typeof e === 'object'
|
||||
&& e !== null
|
||||
&& 'event_id' in e
|
||||
&& 'event_name' in e
|
||||
&& 'person_status' in e,
|
||||
)
|
||||
}
|
||||
catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredEvents(events: PortalEvent[]): void {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
localStorage.setItem(STORAGE_EVENTS, JSON.stringify(events))
|
||||
}
|
||||
|
||||
function readStoredActiveEventId(): string | null {
|
||||
if (typeof localStorage === 'undefined') return null
|
||||
|
||||
return localStorage.getItem(STORAGE_ACTIVE_EVENT)
|
||||
}
|
||||
|
||||
function writeStoredActiveEventId(id: string | null): void {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
if (id) localStorage.setItem(STORAGE_ACTIVE_EVENT, id)
|
||||
else localStorage.removeItem(STORAGE_ACTIVE_EVENT)
|
||||
}
|
||||
|
||||
function mergeEvents(apiEvents: PortalEvent[], stored: PortalEvent[]): PortalEvent[] {
|
||||
const map = new Map<string, PortalEvent>()
|
||||
for (const e of stored) map.set(e.event_id, { ...e })
|
||||
for (const e of apiEvents) {
|
||||
const prev = map.get(e.event_id)
|
||||
map.set(e.event_id, {
|
||||
...prev,
|
||||
...e,
|
||||
organisation_name: e.organisation_name || prev?.organisation_name || '',
|
||||
})
|
||||
}
|
||||
|
||||
return Array.from(map.values()).sort((a, b) => b.start_date.localeCompare(a.start_date))
|
||||
}
|
||||
|
||||
export const usePortalStore = defineStore('portal', () => {
|
||||
const activeEventId = ref<string | null>(readStoredActiveEventId())
|
||||
const userEvents = ref<PortalEvent[]>([])
|
||||
const currentPerson = ref<PortalPersonPayload | null>(null)
|
||||
const isLoadingEvents = ref(false)
|
||||
const isLoadingPerson = ref(false)
|
||||
const loadError = ref<string | null>(null)
|
||||
|
||||
const activeEvent = computed(() => userEvents.value.find(e => e.event_id === activeEventId.value) ?? null)
|
||||
|
||||
function persistActiveEvent(): void {
|
||||
writeStoredActiveEventId(activeEventId.value)
|
||||
}
|
||||
|
||||
function persistEvents(): void {
|
||||
writeStoredEvents(userEvents.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Call after successful public registration so the volunteer sees the event on the dashboard.
|
||||
* TODO: replace with `portal_events` from GET /auth/me when the API exposes it.
|
||||
*/
|
||||
function savePendingEventFromRegistration(event: PortalEvent): void {
|
||||
const merged = mergeEvents([], [...readStoredEvents(), ...userEvents.value, event])
|
||||
userEvents.value = merged
|
||||
persistEvents()
|
||||
if (!activeEventId.value || !merged.some(e => e.event_id === activeEventId.value)) {
|
||||
activeEventId.value = event.event_id
|
||||
persistActiveEvent()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUserEventsFromApiAndStorage(): Promise<void> {
|
||||
isLoadingEvents.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
const stored = readStoredEvents()
|
||||
let apiEvents: PortalEvent[] = []
|
||||
try {
|
||||
const { data } = await apiClient.get<{ success: boolean; data: AuthMeUser }>('/auth/me')
|
||||
apiEvents = data.data.portal_events ?? []
|
||||
}
|
||||
catch {
|
||||
// /auth/me failed — still show locally stored registrations
|
||||
}
|
||||
userEvents.value = mergeEvents(apiEvents, stored)
|
||||
persistEvents()
|
||||
}
|
||||
catch (e) {
|
||||
loadError.value = 'Kon je evenementen niet laden.'
|
||||
userEvents.value = readStoredEvents()
|
||||
}
|
||||
finally {
|
||||
isLoadingEvents.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resolveActiveEventId(): void {
|
||||
if (userEvents.value.length === 0) {
|
||||
activeEventId.value = null
|
||||
persistActiveEvent()
|
||||
|
||||
return
|
||||
}
|
||||
const current = activeEventId.value
|
||||
if (current && userEvents.value.some(e => e.event_id === current)) {
|
||||
persistActiveEvent()
|
||||
|
||||
return
|
||||
}
|
||||
activeEventId.value = userEvents.value[0]!.event_id
|
||||
persistActiveEvent()
|
||||
}
|
||||
|
||||
async function fetchCurrentPerson(): Promise<void> {
|
||||
currentPerson.value = null
|
||||
const eid = activeEventId.value
|
||||
if (!eid) return
|
||||
|
||||
isLoadingPerson.value = true
|
||||
try {
|
||||
const { data } = await apiClient.get<{ success: boolean; data: PortalPersonPayload }>(
|
||||
'/portal/me',
|
||||
{ params: { event_id: eid } },
|
||||
)
|
||||
currentPerson.value = data.data
|
||||
const status = data.data.status
|
||||
const pid = data.data.id
|
||||
userEvents.value = userEvents.value.map(row =>
|
||||
row.event_id === eid ? { ...row, person_id: pid, person_status: status } : row,
|
||||
)
|
||||
persistEvents()
|
||||
}
|
||||
catch {
|
||||
currentPerson.value = null
|
||||
}
|
||||
finally {
|
||||
isLoadingPerson.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function hydrateAfterAuth(): Promise<void> {
|
||||
await loadUserEventsFromApiAndStorage()
|
||||
resolveActiveEventId()
|
||||
await fetchCurrentPerson()
|
||||
}
|
||||
|
||||
function setActiveEvent(eventId: string): void {
|
||||
if (!userEvents.value.some(e => e.event_id === eventId)) return
|
||||
activeEventId.value = eventId
|
||||
persistActiveEvent()
|
||||
void fetchCurrentPerson()
|
||||
}
|
||||
|
||||
function reset(): void {
|
||||
activeEventId.value = null
|
||||
userEvents.value = []
|
||||
currentPerson.value = null
|
||||
loadError.value = null
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(STORAGE_EVENTS)
|
||||
localStorage.removeItem(STORAGE_ACTIVE_EVENT)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeEventId,
|
||||
userEvents,
|
||||
currentPerson,
|
||||
activeEvent,
|
||||
isLoadingEvents,
|
||||
isLoadingPerson,
|
||||
loadError,
|
||||
savePendingEventFromRegistration,
|
||||
hydrateAfterAuth,
|
||||
setActiveEvent,
|
||||
fetchCurrentPerson,
|
||||
reset,
|
||||
}
|
||||
})
|
||||
62
apps/portal/src/types/portal.ts
Normal file
62
apps/portal/src/types/portal.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Volunteer-facing event context for the portal.
|
||||
* Populated from GET /auth/me when the API adds `portal_events`, merged with
|
||||
* locally stored events (e.g. after public registration).
|
||||
*/
|
||||
export interface PortalEvent {
|
||||
event_id: string
|
||||
event_name: string
|
||||
organisation_name: string
|
||||
/** Present when the row was saved from the public registration flow */
|
||||
organisation_id?: string
|
||||
person_id?: string | null
|
||||
person_status: string
|
||||
start_date: string
|
||||
end_date: string
|
||||
}
|
||||
|
||||
/** GET /auth/me — extend when backend adds portal_events */
|
||||
export interface AuthMeUser {
|
||||
id: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
full_name: string
|
||||
email: string
|
||||
timezone?: string
|
||||
locale?: string
|
||||
avatar?: string | null
|
||||
email_verified_at?: string | null
|
||||
organisations?: Array<{
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
role: string
|
||||
}>
|
||||
app_roles?: string[]
|
||||
/** Present on login (`UserResource`); `/auth/me` uses `app_roles` */
|
||||
roles?: string[]
|
||||
permissions?: string[]
|
||||
portal_events?: PortalEvent[]
|
||||
}
|
||||
|
||||
/** GET /portal/me?event_id= — person payload (subset used by dashboard) */
|
||||
export interface PortalPersonPayload {
|
||||
id: string
|
||||
event_id: string
|
||||
status: string
|
||||
full_name: string
|
||||
created_at: string
|
||||
shift_assignments?: Array<{
|
||||
id: string
|
||||
status: string
|
||||
shift?: {
|
||||
id: string
|
||||
festival_section?: { name?: string | null } | null
|
||||
time_slot?: {
|
||||
date?: string
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
} | null
|
||||
} | null
|
||||
}>
|
||||
}
|
||||
3
apps/portal/typed-router.d.ts
vendored
3
apps/portal/typed-router.d.ts
vendored
@@ -26,6 +26,9 @@ declare module 'vue-router/auto-routes' {
|
||||
'portal-profile': RouteRecordInfo<'portal-profile', '/profile', Record<never, never>, Record<never, never>>,
|
||||
'volunteer-register': RouteRecordInfo<'volunteer-register', '/register/:eventSlug', { eventSlug: ParamValue<true> }, { eventSlug: ParamValue<false> }>,
|
||||
'register-success': RouteRecordInfo<'register-success', '/register/success', Record<never, never>, Record<never, never>>,
|
||||
'volunteer-register-info': RouteRecordInfo<'volunteer-register-info', '/registreren', Record<never, never>, Record<never, never>>,
|
||||
'portal-shifts': RouteRecordInfo<'portal-shifts', '/shifts', Record<never, never>, Record<never, never>>,
|
||||
'reset-password': RouteRecordInfo<'reset-password', '/wachtwoord-resetten', Record<never, never>, Record<never, never>>,
|
||||
'forgot-password': RouteRecordInfo<'forgot-password', '/wachtwoord-vergeten', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user