feat: event registration branding with vertical wizard layout

- Add registration_banner_url, registration_welcome_text, registration_logo_url
  columns to events table with migration
- Add uploadImage endpoint (POST .../upload-image) with form request validation
  for banner and logo images (jpg/png/webp, max 5MB)
- Include branding fields in EventResource and PublicRegistrationDataController
- Build registration settings UI in organizer event settings page with
  banner/logo upload and welcome text editor
- Redesign portal registration page: hero banner with gradient overlay,
  welcome text card, vertical step navigation (desktop) / horizontal chips
  (mobile), two-column form fields with density="comfortable"
- Update success page with event banner and consistent branding
- Seed welcome text for Echt Feesten 2026
- Add 9 PHPUnit tests covering image upload, branding fields in API responses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 21:09:49 +02:00
parent 78cc19373e
commit 0d741550a8
16 changed files with 1225 additions and 672 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,4 @@
<script setup lang="ts">
import { useGenerateImageVariant } from '@core/composable/useGenerateImageVariant'
import miscMaskLight from '@images/pages/misc-mask-light.png'
import miscMaskDark from '@images/pages/misc-mask-dark.png'
import { useAuthStore } from '@/stores/useAuthStore'
definePage({
@@ -16,103 +13,98 @@ const route = useRoute('register-success')
const authStore = useAuthStore()
const eventName = computed(() => (route.query.event as string) || 'het evenement')
const bannerUrl = computed(() => (route.query.banner as string) || null)
const isAuthenticated = computed(() => route.query.authenticated === '1' || authStore.isAuthenticated)
const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
</script>
<template>
<!-- Logo -->
<RouterLink to="/">
<div class="auth-logo d-flex align-center gap-x-3">
<VIcon
icon="tabler-users-group"
size="28"
color="primary"
/>
<h1 class="auth-title">
Crewli
</h1>
</div>
</RouterLink>
<VRow
no-gutters
class="auth-wrapper bg-surface"
>
<VCol
cols="12"
class="d-flex align-center justify-center"
<div>
<!-- Event banner (if available) -->
<VImg
v-if="bannerUrl"
:src="bannerUrl"
height="180"
cover
gradient="to bottom, rgba(0,0,0,0.1), rgba(0,0,0,0.4)"
>
<div class="position-relative w-100 d-flex align-center justify-center" style="min-block-size: 100dvh;">
<VCard
flat
:max-width="550"
class="text-center pa-8 pa-sm-12"
style="z-index: 1;"
>
<VAvatar
size="100"
color="success"
variant="tonal"
class="mb-6"
>
<VIcon
icon="tabler-circle-check"
size="60"
/>
</VAvatar>
<h4 class="text-h4 mb-2">
Bedankt voor je aanmelding!
</h4>
<p class="text-body-1 text-medium-emphasis mb-2">
Je aanmelding bij <strong>{{ eventName }}</strong> is succesvol ontvangen.
</p>
<p class="text-body-1 text-medium-emphasis mb-2">
Het organisatieteam beoordeelt je aanmelding zo snel mogelijk.
</p>
<p class="text-body-2 text-disabled mb-8">
Je ontvangt een e-mail zodra je aanmelding is goedgekeurd.
</p>
<div class="d-flex flex-wrap justify-center gap-4">
<VBtn
v-if="isAuthenticated"
to="/dashboard"
color="primary"
prepend-icon="tabler-dashboard"
>
Ga naar je dashboard
</VBtn>
<VBtn
v-else
to="/login"
color="primary"
variant="tonal"
prepend-icon="tabler-login"
>
Heb je al een account? Log in
</VBtn>
</div>
</VCard>
<img
class="auth-footer-mask flip-in-rtl"
:src="authThemeMask"
alt="footer-mask"
height="280"
width="100"
>
<div class="d-flex align-center justify-center fill-height">
<h3 class="text-h5 text-white font-weight-bold">
{{ eventName }}
</h3>
</div>
</VCol>
</VRow>
</template>
</VImg>
<style lang="scss">
@use "@core/scss/template/pages/page-auth.scss";
</style>
<!-- Fallback header -->
<div
v-else
class="d-flex align-center justify-center pa-6"
style="background: rgb(var(--v-theme-primary));"
>
<h3 class="text-h5 text-white font-weight-bold">
{{ eventName }}
</h3>
</div>
<VContainer style="max-inline-size: 600px;">
<VCard
class="text-center pa-8 pa-sm-12 mt-n6"
variant="flat"
style="position: relative; z-index: 1;"
>
<VAvatar
size="100"
color="success"
variant="tonal"
class="mb-6"
>
<VIcon
icon="tabler-circle-check"
size="60"
/>
</VAvatar>
<h4 class="text-h4 mb-4">
Bedankt voor je aanmelding!
</h4>
<p class="text-body-1 text-medium-emphasis mb-2">
Je aanmelding bij <strong>{{ eventName }}</strong> is succesvol ontvangen.
</p>
<p class="text-body-1 text-medium-emphasis mb-2">
Je aanmelding wordt beoordeeld door het organisatieteam.
</p>
<p class="text-body-2 text-disabled mb-8">
Je ontvangt een e-mail zodra je aanmelding is goedgekeurd.
</p>
<div class="d-flex flex-wrap justify-center gap-4">
<VBtn
v-if="isAuthenticated"
to="/dashboard"
color="primary"
prepend-icon="tabler-dashboard"
>
Ga naar je dashboard
</VBtn>
<VBtn
v-else
to="/login"
color="primary"
variant="tonal"
prepend-icon="tabler-login"
>
Heb je al een account? Log in
</VBtn>
</div>
</VCard>
<!-- Footer -->
<div class="text-center pa-4 text-caption text-medium-emphasis">
Powered by Crewli
</div>
</VContainer>
</div>
</template>

View File

@@ -5,6 +5,9 @@ export interface EventRegistrationData {
start_date: string
end_date: string
organisation_id: string
registration_banner_url: string | null
registration_welcome_text: string | null
registration_logo_url: string | null
}
sections: SectionOption[]
time_slots: TimeSlotOption[]