144 lines
3.8 KiB
Vue
144 lines
3.8 KiB
Vue
<template>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
{{ label }}
|
|
<span v-if="!required" class="text-gray-500 text-xs">(optional)</span>
|
|
</label>
|
|
|
|
<div v-if="imageFilename" class="mb-2">
|
|
<div class="relative inline-block">
|
|
<img
|
|
:src="`/api/personen/${imageFilename}?width=100&height=100`"
|
|
:alt="label"
|
|
class="w-24 h-24 object-cover rounded-lg border-2 border-gray-300"
|
|
/>
|
|
<button
|
|
v-if="!uploading"
|
|
@click="removeImage"
|
|
class="absolute -top-2 -right-2 bg-red-600 text-white rounded-full p-1 hover:bg-red-700 transition-colors"
|
|
type="button"
|
|
title="Bild entfernen"
|
|
>
|
|
<X :size="14" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<label
|
|
:class="[
|
|
'flex-1 px-4 py-2 border-2 border-dashed rounded-lg cursor-pointer transition-colors',
|
|
uploading ? 'border-gray-300 bg-gray-50 cursor-not-allowed' :
|
|
dragOver ? 'border-primary-500 bg-primary-50' :
|
|
'border-gray-300 hover:border-primary-400 hover:bg-gray-50'
|
|
]"
|
|
>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp"
|
|
class="hidden"
|
|
:disabled="uploading"
|
|
@change="handleFileSelect"
|
|
/>
|
|
<div class="text-center">
|
|
<div v-if="uploading" class="flex items-center justify-center gap-2 text-gray-600">
|
|
<Loader2 :size="16" class="animate-spin" />
|
|
<span>Wird hochgeladen...</span>
|
|
</div>
|
|
<div v-else class="text-sm text-gray-600">
|
|
<span v-if="!imageFilename">📷 Bild auswählen oder hier ablegen</span>
|
|
<span v-else>🔄 Bild ändern</span>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
<p v-if="error" class="mt-1 text-sm text-red-600">{{ error }}</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import { Loader2, X } from 'lucide-vue-next'
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
label: {
|
|
type: String,
|
|
default: 'Bild'
|
|
},
|
|
required: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue'])
|
|
|
|
const fileInput = ref(null)
|
|
const uploading = ref(false)
|
|
const dragOver = ref(false)
|
|
const error = ref('')
|
|
|
|
const imageFilename = computed(() => props.modelValue)
|
|
|
|
async function handleFileSelect(event) {
|
|
const file = event.target.files?.[0]
|
|
if (!file) return
|
|
|
|
await uploadImage(file)
|
|
}
|
|
|
|
async function uploadImage(file) {
|
|
if (!file.type.match(/^image\/(jpeg|jpg|png|gif|webp)$/)) {
|
|
error.value = 'Nur Bilddateien sind erlaubt (JPEG, PNG, GIF, WebP)'
|
|
return
|
|
}
|
|
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
error.value = 'Bild darf maximal 10MB groß sein'
|
|
return
|
|
}
|
|
|
|
uploading.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('image', file)
|
|
|
|
const response = await fetch('/api/personen/upload', {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'include'
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({ statusMessage: 'Fehler beim Hochladen' }))
|
|
throw new Error(errorData.statusMessage || 'Fehler beim Hochladen')
|
|
}
|
|
|
|
const data = await response.json()
|
|
emit('update:modelValue', data.filename)
|
|
} catch (err) {
|
|
error.value = err.message || 'Fehler beim Hochladen des Bildes'
|
|
console.error('Upload error:', err)
|
|
} finally {
|
|
uploading.value = false
|
|
if (fileInput.value) {
|
|
fileInput.value.value = ''
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeImage() {
|
|
emit('update:modelValue', null)
|
|
error.value = ''
|
|
}
|
|
</script>
|
|
|