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:
Torsten Schulz (local)
2025-12-18 13:37:03 +01:00
parent a004ffba9b
commit baf6c59c0d
14 changed files with 756 additions and 186 deletions

143
components/ImageUpload.vue Normal file
View 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>