WS-3 session 1b-i Tier 1. Scope: src/components/**, src/pages/**, src/layouts/**, src/views/** restricted to *.vue files. Mechanical formatting only — predominantly vue/html-indent (506 fixes in CrowdListDetailPanel.vue alone), padding-line-between-statements, antfu/if-newline. Excludes (per session prompt): - apps/app/vite.config.ts (Tier 3) - apps/app/themeConfig.ts (Tier 3) - apps/app/vitest.config.ts (Tier 3) - All TypeScript-only files in src/composables, src/lib, src/stores, src/plugins, src/types (Tier 2 — separate commit) Includes session 1a layouts (PortalLayout.vue, PublicLayout.vue) where 2 'lines-around-comment' errors were flagged in the previous 1b-i pre-flight inspection. Tests + typecheck verified green post-fix: - apps/app vitest: 49 passed (unchanged) - apps/app vue-tsc: clean (unchanged) - apps/portal vitest: 113 passed (unchanged — not touched) - backend pest: 1486 passed (unchanged — not touched) Lint baseline progression: - Pre-Tier-1: 1451 problems - Post-Tier-1: 422 problems Visual smoke status: - NOT YET SMOKED — Bert to verify before merge. This Claude Code session has no UI access; cannot run pnpm dev and click through affected routes. The high-traffic candidates are CrowdListDetailPanel (506 fixes), AssignPersonDialog (44), ShiftDetailPanel (36), and the events / form-failures pages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
136 lines
2.7 KiB
Vue
136 lines
2.7 KiB
Vue
<script setup lang="ts">
|
|
import { apiClient } from '@/lib/axios'
|
|
|
|
interface Props {
|
|
modelValue: string | null
|
|
label?: string
|
|
purpose: 'logo' | 'banner' | 'icon' | 'avatar'
|
|
hint?: string
|
|
previewHeight?: number
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
label: undefined,
|
|
hint: undefined,
|
|
previewHeight: 120,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [url: string | null]
|
|
}>()
|
|
|
|
const fileInput = ref<HTMLInputElement | null>(null)
|
|
const uploading = ref(false)
|
|
const error = ref('')
|
|
|
|
function triggerFileInput() {
|
|
fileInput.value?.click()
|
|
}
|
|
|
|
async function onFileSelected(event: Event) {
|
|
const input = event.target as HTMLInputElement
|
|
const file = input.files?.[0]
|
|
if (!file)
|
|
return
|
|
|
|
uploading.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
const formData = new FormData()
|
|
|
|
formData.append('file', file)
|
|
formData.append('purpose', props.purpose)
|
|
|
|
const { data } = await apiClient.post<{ data: { url: string } }>('/upload/image', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
})
|
|
|
|
emit('update:modelValue', data.data.url)
|
|
}
|
|
catch (e: unknown) {
|
|
const axiosError = e as { response?: { data?: { message?: string } } }
|
|
|
|
error.value = axiosError.response?.data?.message ?? 'Upload mislukt'
|
|
}
|
|
finally {
|
|
uploading.value = false
|
|
if (input)
|
|
input.value = ''
|
|
}
|
|
}
|
|
|
|
function remove() {
|
|
emit('update:modelValue', null)
|
|
error.value = ''
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<label
|
|
v-if="label"
|
|
class="text-body-2 text-medium-emphasis d-block mb-1"
|
|
>
|
|
{{ label }}
|
|
</label>
|
|
|
|
<div
|
|
v-if="modelValue"
|
|
class="mb-2"
|
|
>
|
|
<VImg
|
|
:src="modelValue"
|
|
:max-height="previewHeight"
|
|
max-width="300"
|
|
class="rounded border"
|
|
cover
|
|
/>
|
|
</div>
|
|
|
|
<div class="d-flex align-center gap-2">
|
|
<VBtn
|
|
:variant="modelValue ? 'tonal' : 'elevated'"
|
|
color="primary"
|
|
size="small"
|
|
prepend-icon="tabler-upload"
|
|
:loading="uploading"
|
|
@click="triggerFileInput"
|
|
>
|
|
{{ modelValue ? 'Vervangen' : 'Uploaden' }}
|
|
</VBtn>
|
|
<VBtn
|
|
v-if="modelValue"
|
|
variant="text"
|
|
color="error"
|
|
size="small"
|
|
prepend-icon="tabler-trash"
|
|
@click="remove"
|
|
>
|
|
Verwijderen
|
|
</VBtn>
|
|
</div>
|
|
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
accept="image/*"
|
|
style="display: none;"
|
|
@change="onFileSelected"
|
|
>
|
|
|
|
<div
|
|
v-if="error"
|
|
class="text-caption text-error mt-1"
|
|
>
|
|
{{ error }}
|
|
</div>
|
|
<div
|
|
v-else-if="hint"
|
|
class="text-caption text-medium-emphasis mt-1"
|
|
>
|
|
{{ hint }}
|
|
</div>
|
|
</div>
|
|
</template>
|