Enhance Vereinsmeisterschaften and Vorstand pages with image support for players and board members. Implement lightbox functionality for player images in Vereinsmeisterschaften. Update CSV handling to include image filenames for better data management. Refactor components to utilize PersonCard for board members, improving code readability and maintainability.
This commit is contained in:
143
components/ImageUpload.vue
Normal file
143
components/ImageUpload.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user