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

View File

@@ -7,6 +7,7 @@ namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\StoreEventRequest; use App\Http\Requests\Api\V1\StoreEventRequest;
use App\Http\Requests\Api\V1\UpdateEventRequest; use App\Http\Requests\Api\V1\UpdateEventRequest;
use App\Http\Requests\Api\V1\UploadEventImageRequest;
use App\Http\Resources\Api\V1\EventResource; use App\Http\Resources\Api\V1\EventResource;
use App\Models\Event; use App\Models\Event;
use App\Models\Organisation; use App\Models\Organisation;
@@ -15,6 +16,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
final class EventController extends Controller final class EventController extends Controller
{ {
@@ -126,6 +128,24 @@ final class EventController extends Controller
return EventResource::collection($children); return EventResource::collection($children);
} }
public function uploadImage(UploadEventImageRequest $request, Organisation $organisation, Event $event): JsonResponse
{
Gate::authorize('update', [$event, $organisation]);
$path = $request->file('image')->store(
"events/{$event->id}",
'public'
);
$field = $request->type === 'banner'
? 'registration_banner_url'
: 'registration_logo_url';
$event->update([$field => Storage::disk('public')->url($path)]);
return response()->json(['url' => $event->fresh()->{$field}]);
}
public function stats(Event $event): JsonResponse public function stats(Event $event): JsonResponse
{ {
Gate::authorize('view', $event); Gate::authorize('view', $event);

View File

@@ -60,6 +60,9 @@ final class PublicRegistrationDataController extends Controller
'start_date' => $festivalEvent->start_date->toDateString(), 'start_date' => $festivalEvent->start_date->toDateString(),
'end_date' => $festivalEvent->end_date->toDateString(), 'end_date' => $festivalEvent->end_date->toDateString(),
'organisation_id' => $festivalEvent->organisation_id, 'organisation_id' => $festivalEvent->organisation_id,
'registration_banner_url' => $festivalEvent->registration_banner_url,
'registration_welcome_text' => $festivalEvent->registration_welcome_text,
'registration_logo_url' => $festivalEvent->registration_logo_url,
], ],
'sections' => $sections->map(fn (FestivalSection $section) => [ 'sections' => $sections->map(fn (FestivalSection $section) => [
'id' => $section->id, 'id' => $section->id,

View File

@@ -27,6 +27,7 @@ final class UpdateEventRequest extends FormRequest
'event_type' => ['sometimes', 'in:event,festival,series'], 'event_type' => ['sometimes', 'in:event,festival,series'],
'event_type_label' => ['nullable', 'string', 'max:50'], 'event_type_label' => ['nullable', 'string', 'max:50'],
'sub_event_label' => ['nullable', 'string', 'max:50'], 'sub_event_label' => ['nullable', 'string', 'max:50'],
'registration_welcome_text' => ['nullable', 'string', 'max:1000'],
]; ];
} }
} }

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Api\V1;
use Illuminate\Foundation\Http\FormRequest;
final class UploadEventImageRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, mixed> */
public function rules(): array
{
return [
'image' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
'type' => ['required', 'in:banner,logo'],
];
}
}

View File

@@ -27,6 +27,9 @@ final class EventResource extends JsonResource
'event_type_label' => $this->event_type_label, 'event_type_label' => $this->event_type_label,
'sub_event_label' => $this->sub_event_label, 'sub_event_label' => $this->sub_event_label,
'is_recurring' => $this->is_recurring, 'is_recurring' => $this->is_recurring,
'registration_banner_url' => $this->registration_banner_url,
'registration_welcome_text' => $this->registration_welcome_text,
'registration_logo_url' => $this->registration_logo_url,
'is_festival' => $this->resource->isFestival(), 'is_festival' => $this->resource->isFestival(),
'is_sub_event' => $this->resource->isSubEvent(), 'is_sub_event' => $this->resource->isSubEvent(),
'is_flat_event' => $this->resource->isFlatEvent(), 'is_flat_event' => $this->resource->isFlatEvent(),

View File

@@ -63,6 +63,9 @@ final class Event extends Model
'is_recurring', 'is_recurring',
'recurrence_rule', 'recurrence_rule',
'recurrence_exceptions', 'recurrence_exceptions',
'registration_banner_url',
'registration_welcome_text',
'registration_logo_url',
]; ];
protected function casts(): array protected function casts(): array

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('events', function (Blueprint $table) {
$table->string('registration_banner_url')->nullable()->after('recurrence_exceptions');
$table->text('registration_welcome_text')->nullable()->after('registration_banner_url');
$table->string('registration_logo_url')->nullable()->after('registration_welcome_text');
});
}
public function down(): void
{
Schema::table('events', function (Blueprint $table) {
$table->dropColumn([
'registration_banner_url',
'registration_welcome_text',
'registration_logo_url',
]);
});
}
};

View File

@@ -175,6 +175,7 @@ class DevSeeder extends Seeder
'event_type' => 'festival', 'event_type' => 'festival',
'event_type_label' => 'Festival', 'event_type_label' => 'Festival',
'sub_event_label' => 'Programmaonderdeel', 'sub_event_label' => 'Programmaonderdeel',
'registration_welcome_text' => 'Wij zoeken enthousiaste vrijwilligers voor Echt Feesten 2026! Word onderdeel van ons team en beleef het festival van de andere kant. Gratis toegang, maaltijden, en een onvergetelijke ervaring.',
]); ]);
$vrijdag = Event::create([ $vrijdag = Event::create([

View File

@@ -77,6 +77,7 @@ Route::middleware('auth:sanctum')->group(function () {
->only(['index', 'show', 'store', 'update', 'destroy']); ->only(['index', 'show', 'store', 'update', 'destroy']);
Route::get('organisations/{organisation}/events/{event}/children', [EventController::class, 'children']); Route::get('organisations/{organisation}/events/{event}/children', [EventController::class, 'children']);
Route::post('organisations/{organisation}/events/{event}/transition', [EventController::class, 'transition']); Route::post('organisations/{organisation}/events/{event}/transition', [EventController::class, 'transition']);
Route::post('organisations/{organisation}/events/{event}/upload-image', [EventController::class, 'uploadImage']);
// Organisation-scoped resources // Organisation-scoped resources
Route::prefix('organisations/{organisation}')->group(function () { Route::prefix('organisations/{organisation}')->group(function () {

View File

@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Api\V1;
use App\Models\Event;
use App\Models\Organisation;
use App\Models\User;
use Database\Seeders\RoleSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
class EventImageUploadTest extends TestCase
{
use RefreshDatabase;
private Organisation $organisation;
private User $orgAdmin;
private Event $event;
protected function setUp(): void
{
parent::setUp();
$this->seed(RoleSeeder::class);
$this->organisation = Organisation::factory()->create();
$this->orgAdmin = User::factory()->create();
$this->orgAdmin->assignRole('org_admin');
$this->organisation->users()->attach($this->orgAdmin, ['role' => 'org_admin']);
$this->event = Event::factory()->create([
'organisation_id' => $this->organisation->id,
]);
Storage::fake('public');
}
public function test_upload_banner_jpg(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->postJson(
"/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/upload-image",
[
'image' => UploadedFile::fake()->image('banner.jpg', 1200, 400),
'type' => 'banner',
]
);
$response->assertOk()
->assertJsonStructure(['url']);
$this->event->refresh();
$this->assertNotNull($this->event->registration_banner_url);
}
public function test_upload_logo_png(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->postJson(
"/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/upload-image",
[
'image' => UploadedFile::fake()->image('logo.png', 200, 200),
'type' => 'logo',
]
);
$response->assertOk()
->assertJsonStructure(['url']);
$this->event->refresh();
$this->assertNotNull($this->event->registration_logo_url);
}
public function test_upload_rejects_invalid_file_type(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->postJson(
"/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/upload-image",
[
'image' => UploadedFile::fake()->create('document.pdf', 100, 'application/pdf'),
'type' => 'banner',
]
);
$response->assertUnprocessable();
}
public function test_upload_rejects_file_too_large(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->postJson(
"/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/upload-image",
[
'image' => UploadedFile::fake()->image('large.jpg')->size(6000),
'type' => 'banner',
]
);
$response->assertUnprocessable();
}
public function test_upload_requires_authentication(): void
{
$response = $this->postJson(
"/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/upload-image",
[
'image' => UploadedFile::fake()->image('banner.jpg'),
'type' => 'banner',
]
);
$response->assertUnauthorized();
}
public function test_upload_rejects_wrong_organisation(): void
{
$otherOrg = Organisation::factory()->create();
Sanctum::actingAs($this->orgAdmin);
$response = $this->postJson(
"/api/v1/organisations/{$otherOrg->id}/events/{$this->event->id}/upload-image",
[
'image' => UploadedFile::fake()->image('banner.jpg'),
'type' => 'banner',
]
);
$response->assertForbidden();
}
public function test_registration_data_includes_branding_fields(): void
{
$event = Event::factory()->create([
'organisation_id' => $this->organisation->id,
'status' => 'registration_open',
'slug' => 'branding-test-event',
'registration_welcome_text' => 'Welcome to our event!',
'registration_banner_url' => 'https://example.com/banner.jpg',
'registration_logo_url' => 'https://example.com/logo.png',
]);
$response = $this->getJson('/api/v1/public/events/branding-test-event/registration-data');
$response->assertOk()
->assertJsonPath('data.event.registration_welcome_text', 'Welcome to our event!')
->assertJsonPath('data.event.registration_banner_url', 'https://example.com/banner.jpg')
->assertJsonPath('data.event.registration_logo_url', 'https://example.com/logo.png');
}
public function test_registration_data_includes_null_branding_fields(): void
{
Event::factory()->create([
'organisation_id' => $this->organisation->id,
'status' => 'registration_open',
'slug' => 'no-branding-event',
]);
$response = $this->getJson('/api/v1/public/events/no-branding-event/registration-data');
$response->assertOk()
->assertJsonPath('data.event.registration_welcome_text', null)
->assertJsonPath('data.event.registration_banner_url', null)
->assertJsonPath('data.event.registration_logo_url', null);
}
public function test_event_update_saves_welcome_text(): void
{
Sanctum::actingAs($this->orgAdmin);
$response = $this->putJson(
"/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}",
[
'registration_welcome_text' => 'We zoeken vrijwilligers!',
]
);
$response->assertOk();
$this->event->refresh();
$this->assertEquals('We zoeken vrijwilligers!', $this->event->registration_welcome_text);
}
}

View File

@@ -132,6 +132,29 @@ export function useUpdateEvent(orgId: Ref<string>, id: Ref<string>) {
}) })
} }
export function useUploadEventImage(orgId: Ref<string>, eventId: Ref<string>) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ file, type }: { file: File; type: 'banner' | 'logo' }) => {
const formData = new FormData()
formData.append('image', file)
formData.append('type', type)
const { data } = await apiClient.post<{ url: string }>(
`/organisations/${orgId.value}/events/${eventId.value}/upload-image`,
formData,
{ headers: { 'Content-Type': 'multipart/form-data' } },
)
return data.url
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['events', orgId.value, eventId.value] })
},
})
}
export function useEventStats(eventId: Ref<string>) { export function useEventStats(eventId: Ref<string>) {
return useQuery({ return useQuery({
queryKey: ['events', eventId, 'stats'], queryKey: ['events', eventId, 'stats'],

View File

@@ -1,19 +1,234 @@
<script setup lang="ts"> <script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import EventTabsNav from '@/components/events/EventTabsNav.vue' import EventTabsNav from '@/components/events/EventTabsNav.vue'
import { useUpdateEvent, useUploadEventImage } from '@/composables/api/useEvents'
import { useAuthStore } from '@/stores/useAuthStore'
import type { EventItem } from '@/types/event'
definePage({ definePage({
meta: { meta: {
navActiveLink: 'events', navActiveLink: 'events',
}, },
}) })
const route = useRoute()
const authStore = useAuthStore()
const orgId = computed(() => authStore.currentOrganisation?.id ?? '')
const eventId = computed(() => String((route.params as { id: string }).id))
const { mutate: updateEvent, isPending: isUpdating } = useUpdateEvent(orgId, eventId)
const { mutate: uploadImage, isPending: isUploading } = useUploadEventImage(orgId, eventId)
const welcomeText = ref('')
const showSuccess = ref(false)
const refVForm = ref<VForm>()
function initForm(event: EventItem) {
welcomeText.value = event.registration_welcome_text ?? ''
}
function onSaveWelcomeText() {
updateEvent(
{ registration_welcome_text: welcomeText.value || null },
{
onSuccess: () => {
showSuccess.value = true
},
},
)
}
function onFileSelected(files: File[], type: 'banner' | 'logo') {
const file = files[0]
if (!file) return
uploadImage({ file, type })
}
function onClearImage(event: EventItem, type: 'banner' | 'logo') {
const field = type === 'banner' ? 'registration_banner_url' : 'registration_logo_url'
updateEvent({ [field]: null } as any)
}
</script> </script>
<template> <template>
<EventTabsNav> <EventTabsNav v-slot="{ event }">
<VCard class="ma-4"> <div @vue:mounted="initForm(event)">
<VCardText> <VRow>
Deze module is binnenkort beschikbaar. <VCol
</VCardText> cols="12"
</VCard> md="8"
>
<!-- Registration Branding -->
<VCard class="mb-6">
<VCardTitle class="d-flex align-center gap-2">
<VIcon
icon="tabler-palette"
size="20"
/>
Registratie-uiterlijk
</VCardTitle>
<VCardSubtitle>
Pas het uiterlijk van het vrijwilligersregistratieformulier aan
</VCardSubtitle>
<VCardText>
<!-- Banner Image -->
<div class="mb-6">
<h6 class="text-subtitle-1 font-weight-medium mb-2">
Bannerafbeelding
</h6>
<p class="text-body-2 text-medium-emphasis mb-3">
Wordt bovenaan het registratieformulier getoond. Aanbevolen: 1200x400px.
</p>
<VImg
v-if="event.registration_banner_url"
:src="event.registration_banner_url"
height="160"
cover
rounded="lg"
class="mb-3"
/>
<div class="d-flex gap-2">
<VFileInput
accept="image/jpeg,image/png,image/webp"
label="Afbeelding uploaden"
prepend-icon=""
prepend-inner-icon="tabler-upload"
density="compact"
variant="outlined"
hide-details
:loading="isUploading"
class="flex-grow-1"
style="max-inline-size: 350px;"
@update:model-value="(files: File[]) => onFileSelected(files, 'banner')"
/>
<VBtn
v-if="event.registration_banner_url"
variant="outlined"
color="error"
density="compact"
@click="onClearImage(event, 'banner')"
>
Verwijder
</VBtn>
</div>
</div>
<VDivider class="mb-6" />
<!-- Logo Image -->
<div class="mb-6">
<h6 class="text-subtitle-1 font-weight-medium mb-2">
Logo
</h6>
<p class="text-body-2 text-medium-emphasis mb-3">
Wordt in de header van het registratieformulier getoond. Aanbevolen: vierkant, max 200x200px.
</p>
<VAvatar
v-if="event.registration_logo_url"
:image="event.registration_logo_url"
size="80"
rounded="lg"
class="mb-3"
/>
<div class="d-flex gap-2">
<VFileInput
accept="image/jpeg,image/png,image/webp"
label="Logo uploaden"
prepend-icon=""
prepend-inner-icon="tabler-upload"
density="compact"
variant="outlined"
hide-details
:loading="isUploading"
class="flex-grow-1"
style="max-inline-size: 350px;"
@update:model-value="(files: File[]) => onFileSelected(files, 'logo')"
/>
<VBtn
v-if="event.registration_logo_url"
variant="outlined"
color="error"
density="compact"
@click="onClearImage(event, 'logo')"
>
Verwijder
</VBtn>
</div>
</div>
<VDivider class="mb-6" />
<!-- Welcome Text -->
<div>
<h6 class="text-subtitle-1 font-weight-medium mb-2">
Welkomstbericht
</h6>
<VForm
ref="refVForm"
@submit.prevent="onSaveWelcomeText"
>
<VTextarea
v-model="welcomeText"
label="Welkomstbericht"
hint="Dit bericht wordt getoond boven het vrijwilligersregistratieformulier."
persistent-hint
:counter="1000"
rows="4"
auto-grow
class="mb-4"
/>
<VBtn
type="submit"
color="primary"
:loading="isUpdating"
>
Opslaan
</VBtn>
</VForm>
</div>
</VCardText>
</VCard>
</VCol>
<VCol
cols="12"
md="4"
>
<!-- Preview hint -->
<VCard variant="tonal">
<VCardText>
<div class="d-flex align-center gap-2 mb-2">
<VIcon
icon="tabler-eye"
size="18"
/>
<span class="text-subtitle-2 font-weight-medium">Preview</span>
</div>
<p class="text-body-2 text-medium-emphasis mb-0">
Open het registratieformulier om een preview te zien van de aanpassingen.
</p>
</VCardText>
</VCard>
</VCol>
</VRow>
</div>
<VSnackbar
v-model="showSuccess"
color="success"
:timeout="3000"
>
Registratie-instellingen opgeslagen
</VSnackbar>
</EventTabsNav> </EventTabsNav>
</template> </template>

View File

@@ -20,6 +20,9 @@ export interface EventItem {
event_type_label: string | null event_type_label: string | null
sub_event_label: string | null sub_event_label: string | null
is_recurring: boolean is_recurring: boolean
registration_banner_url: string | null
registration_welcome_text: string | null
registration_logo_url: string | null
is_festival: boolean is_festival: boolean
is_sub_event: boolean is_sub_event: boolean
is_flat_event: boolean is_flat_event: boolean
@@ -47,6 +50,7 @@ export interface CreateEventPayload {
export interface UpdateEventPayload extends Partial<CreateEventPayload> { export interface UpdateEventPayload extends Partial<CreateEventPayload> {
status?: EventStatus status?: EventStatus
registration_welcome_text?: string | null
} }
export interface EventStats { export interface EventStats {

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,4 @@
<script setup lang="ts"> <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' import { useAuthStore } from '@/stores/useAuthStore'
definePage({ definePage({
@@ -16,103 +13,98 @@ const route = useRoute('register-success')
const authStore = useAuthStore() const authStore = useAuthStore()
const eventName = computed(() => (route.query.event as string) || 'het evenement') 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 isAuthenticated = computed(() => route.query.authenticated === '1' || authStore.isAuthenticated)
const authThemeMask = useGenerateImageVariant(miscMaskLight, miscMaskDark)
</script> </script>
<template> <template>
<!-- Logo --> <div>
<RouterLink to="/"> <!-- Event banner (if available) -->
<div class="auth-logo d-flex align-center gap-x-3"> <VImg
<VIcon v-if="bannerUrl"
icon="tabler-users-group" :src="bannerUrl"
size="28" height="180"
color="primary" cover
/> gradient="to bottom, rgba(0,0,0,0.1), rgba(0,0,0,0.4)"
<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 class="position-relative w-100 d-flex align-center justify-center" style="min-block-size: 100dvh;"> <div class="d-flex align-center justify-center fill-height">
<VCard <h3 class="text-h5 text-white font-weight-bold">
flat {{ eventName }}
:max-width="550" </h3>
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> </div>
</VCol> </VImg>
</VRow>
</template>
<style lang="scss"> <!-- Fallback header -->
@use "@core/scss/template/pages/page-auth.scss"; <div
</style> 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 start_date: string
end_date: string end_date: string
organisation_id: string organisation_id: string
registration_banner_url: string | null
registration_welcome_text: string | null
registration_logo_url: string | null
} }
sections: SectionOption[] sections: SectionOption[]
time_slots: TimeSlotOption[] time_slots: TimeSlotOption[]