feat(portal): login, dashboard, event switcher, password reset flow

Made-with: Cursor
This commit is contained in:
2026-04-13 00:52:04 +02:00
parent ec4ba8733d
commit 34eb790b3e
16 changed files with 1151 additions and 394 deletions

View File

@@ -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']
}
}

View 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>

View 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>

View File

@@ -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

View File

@@ -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'
}
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: {

View 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>

View 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>

View 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>

View File

@@ -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
}

View File

@@ -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,
}
})

View 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,
}
})

View 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
}>
}

View File

@@ -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>>,
}
}