fix: compact options layout, consistent ImageUploadField across app

- Replace card-based multi-line options with compact single-line rows
  (grip + label + description + delete all on one row)
- Standardize event registration appearance page on ImageUploadField
  (was VFileInput + manual preview, now consistent with email branding)
- Fix EmailBrandingTab logoUrl ref to properly handle null from
  ImageUploadField, ensuring existing image preview works on page load

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 19:15:03 +02:00
parent 6a8d21a5b6
commit b647d2827a
4 changed files with 96 additions and 172 deletions

View File

@@ -323,46 +323,39 @@ defineExpose({ setErrors })
<div
v-for="(option, index) in form.options"
:key="index"
class="mb-3"
class="mb-2"
>
<VCard
variant="outlined"
class="pa-3"
>
<div class="d-flex align-start gap-2">
<VIcon
icon="tabler-grip-vertical"
class="mt-2 text-medium-emphasis"
style="cursor: grab;"
/>
<div class="flex-grow-1">
<AppTextField
v-model="option.label"
:placeholder="`Optie ${index + 1}`"
density="compact"
hide-details="auto"
class="mb-2"
/>
<AppTextField
v-model="option.description"
label="Beschrijving (optioneel)"
density="compact"
placeholder="Korte toelichting die onder de optie verschijnt"
counter="200"
maxlength="200"
hide-details="auto"
/>
</div>
<VBtn
icon="tabler-trash"
variant="text"
color="error"
size="small"
class="mt-1"
@click="removeOption(index)"
/>
</div>
</VCard>
<div class="d-flex align-center gap-2">
<VIcon
icon="tabler-grip-vertical"
size="16"
class="text-medium-emphasis flex-shrink-0"
style="cursor: grab;"
/>
<AppTextField
v-model="option.label"
placeholder="Optie"
density="compact"
hide-details
class="flex-grow-1"
style="max-inline-size: 200px;"
/>
<AppTextField
v-model="option.description"
placeholder="Beschrijving (optioneel)"
density="compact"
hide-details
class="flex-grow-1"
/>
<VBtn
icon="tabler-trash"
variant="text"
color="error"
size="x-small"
class="flex-shrink-0"
@click="removeOption(index)"
/>
</div>
</div>
<div

View File

@@ -19,7 +19,7 @@ const form = ref<InstanceType<typeof VForm>>()
const snackbar = ref(false)
const serverErrors = ref<Record<string, string[]>>({})
const logoUrl = ref('')
const logoUrl = ref<string | null>(null)
const primaryColor = ref('#6366F1')
const secondaryColor = ref('#4F46E5')
const footerText = ref('')
@@ -31,7 +31,7 @@ const showSecondaryPicker = ref(false)
watch(settings, s => {
if (s) {
logoUrl.value = s.logo_url ?? ''
logoUrl.value = s.logo_url ?? null
primaryColor.value = s.primary_color ?? '#6366F1'
secondaryColor.value = s.secondary_color ?? '#4F46E5'
footerText.value = s.footer_text ?? ''

View File

@@ -562,46 +562,39 @@ function activate(template: RegistrationFieldTemplate) {
<div
v-for="(option, index) in form.options"
:key="index"
class="mb-3"
class="mb-2"
>
<VCard
variant="outlined"
class="pa-3"
>
<div class="d-flex align-start gap-2">
<VIcon
icon="tabler-grip-vertical"
class="mt-2 text-medium-emphasis"
style="cursor: grab;"
/>
<div class="flex-grow-1">
<AppTextField
v-model="option.label"
:placeholder="`Optie ${index + 1}`"
density="compact"
hide-details="auto"
class="mb-2"
/>
<AppTextField
v-model="option.description"
label="Beschrijving (optioneel)"
density="compact"
placeholder="Korte toelichting die onder de optie verschijnt"
counter="200"
maxlength="200"
hide-details="auto"
/>
</div>
<VBtn
icon="tabler-trash"
variant="text"
color="error"
size="small"
class="mt-1"
@click="removeOption(index)"
/>
</div>
</VCard>
<div class="d-flex align-center gap-2">
<VIcon
icon="tabler-grip-vertical"
size="16"
class="text-medium-emphasis flex-shrink-0"
style="cursor: grab;"
/>
<AppTextField
v-model="option.label"
placeholder="Optie"
density="compact"
hide-details
class="flex-grow-1"
style="max-inline-size: 200px;"
/>
<AppTextField
v-model="option.description"
placeholder="Beschrijving (optioneel)"
density="compact"
hide-details
class="flex-grow-1"
/>
<VBtn
icon="tabler-trash"
variant="text"
color="error"
size="x-small"
class="flex-shrink-0"
@click="removeOption(index)"
/>
</div>
</div>
<div

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { VForm } from 'vuetify/components/VForm'
import EventTabsNav from '@/components/events/EventTabsNav.vue'
import { useUpdateEvent, useUploadEventImage } from '@/composables/api/useEvents'
import ImageUploadField from '@/components/common/ImageUploadField.vue'
import { useUpdateEvent } from '@/composables/api/useEvents'
import { useAuthStore } from '@/stores/useAuthStore'
import type { EventItem, UpdateEventPayload } from '@/types/event'
@@ -24,16 +25,29 @@ const settingsTab = computed(() => {
})
const { mutate: updateEvent, isPending: isUpdating } = useUpdateEvent(orgId, eventId)
const { mutate: uploadImage, isPending: isUploading } = useUploadEventImage(orgId, eventId)
const bannerUrl = ref<string | null>(null)
const logoUrl = ref<string | null>(null)
const welcomeText = ref('')
const showSuccess = ref(false)
const refVForm = ref<VForm>()
function initForm(event: EventItem) {
bannerUrl.value = event.registration_banner_url ?? null
logoUrl.value = event.registration_logo_url ?? null
welcomeText.value = event.registration_welcome_text ?? ''
}
function onBannerChange(url: string | null) {
bannerUrl.value = url
updateEvent({ registration_banner_url: url } as UpdateEventPayload)
}
function onLogoChange(url: string | null) {
logoUrl.value = url
updateEvent({ registration_logo_url: url } as UpdateEventPayload)
}
function onSaveWelcomeText() {
updateEvent(
{ registration_welcome_text: welcomeText.value || null },
@@ -44,19 +58,6 @@ function onSaveWelcomeText() {
},
)
}
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 UpdateEventPayload)
}
</script>
<template>
@@ -104,91 +105,28 @@ function onClearImage(event: EventItem, type: 'banner' | 'logo') {
<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"
<ImageUploadField
:model-value="bannerUrl"
label="Bannerafbeelding"
purpose="banner"
hint="Wordt bovenaan het registratieformulier getoond. Aanbevolen: 1200x400px."
:preview-height="160"
@update:model-value="onBannerChange"
/>
<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"
<ImageUploadField
:model-value="logoUrl"
label="Logo"
purpose="logo"
hint="Wordt in de header van het registratieformulier getoond. Aanbevolen: vierkant, max 200x200px."
:preview-height="80"
@update:model-value="onLogoChange"
/>
<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" />