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:
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
api/app/Http/Requests/Api/V1/UploadEventImageRequest.php
Normal file
24
api/app/Http/Requests/Api/V1/UploadEventImageRequest.php
Normal 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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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([
|
||||||
|
|||||||
@@ -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 () {
|
||||||
|
|||||||
191
api/tests/Feature/Api/V1/EventImageUploadTest.php
Normal file
191
api/tests/Feature/Api/V1/EventImageUploadTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
Reference in New Issue
Block a user