feat(portal): login, dashboard, event switcher, password reset flow
Made-with: Cursor
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user