This commit updates the Navigation component to replace links for "Über uns", "Geschichte", "TT-Regeln", "Satzung", and "Termine" with a consolidated "Inhalte" and "Sportbetrieb" section. Additionally, it removes the corresponding pages for "Geschichte", "Mannschaften", "Satzung", "Termine", and "Spielpläne" to streamline the CMS structure and improve content management efficiency.
195 lines
15 KiB
Vue
195 lines
15 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-xl sm:text-2xl font-display font-bold text-gray-900">Spielpläne bearbeiten</h2>
|
|
<div class="space-x-3">
|
|
<button class="inline-flex items-center px-3 py-1.5 sm:px-4 sm:py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 text-sm sm:text-base" @click="showUploadModal = true">
|
|
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg>
|
|
CSV hochladen
|
|
</button>
|
|
<button class="inline-flex items-center px-3 py-1.5 sm:px-4 sm:py-2 rounded-lg bg-primary-600 text-white hover:bg-primary-700 text-sm sm:text-base" @click="save">Speichern</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- CSV Upload Section -->
|
|
<div class="mb-8 bg-white rounded-xl shadow-lg p-6">
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">Vereins-Spielplan (CSV)</h3>
|
|
<div v-if="currentFile" class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<svg class="w-5 h-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
<div><p class="text-sm font-medium text-green-800">{{ currentFile.name }}</p><p class="text-xs text-green-600">{{ currentFile.size }} bytes</p></div>
|
|
</div>
|
|
<button class="px-3 py-1 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors" @click="removeFile">Entfernen</button>
|
|
</div>
|
|
</div>
|
|
<div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-primary-400 hover:bg-primary-50 transition-colors cursor-pointer" :class="{ 'border-primary-400 bg-primary-50': isDragOver }" @click="triggerFileInput" @dragover.prevent @dragenter.prevent @drop.prevent="handleFileDrop">
|
|
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg>
|
|
<p class="text-lg font-medium text-gray-900 mb-2">CSV-Datei hochladen</p>
|
|
<p class="text-sm text-gray-600 mb-4">Klicken Sie hier oder ziehen Sie eine CSV-Datei hierher</p>
|
|
</div>
|
|
<input ref="fileInput" type="file" accept=".csv" class="hidden" @change="handleFileSelect">
|
|
</div>
|
|
|
|
<!-- Column Selection -->
|
|
<div v-if="csvData.length > 0 && !columnsSelected" class="bg-white rounded-xl shadow-lg p-6 mb-8">
|
|
<h3 class="text-xl font-semibold text-gray-900 mb-4">Spalten auswählen</h3>
|
|
<p class="text-sm text-gray-600 mb-6">Wählen Sie die Spalten aus, die für den Spielplan gespeichert werden sollen:</p>
|
|
<div class="space-y-4">
|
|
<div v-for="(header, index) in csvHeaders" :key="index" class="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:bg-gray-50">
|
|
<div class="flex items-center">
|
|
<input :id="`column-${index}`" v-model="selectedColumns[index]" type="checkbox" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded">
|
|
<label :for="`column-${index}`" class="ml-3 text-sm font-medium text-gray-900">{{ header }}</label>
|
|
</div>
|
|
<div class="text-xs text-gray-500">{{ getColumnPreview(index) }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-6 flex justify-between items-center">
|
|
<div class="text-sm text-gray-600">{{ selectedColumnsCount }} von {{ csvHeaders.length }} Spalten ausgewählt</div>
|
|
<div class="space-x-3">
|
|
<button class="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" @click="selectAllColumns">Alle auswählen</button>
|
|
<button class="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors" @click="deselectAllColumns">Alle abwählen</button>
|
|
<button class="px-4 py-2 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors" @click="suggestHalleColumns">Halle-Spalten vorschlagen</button>
|
|
<button :disabled="selectedColumnsCount === 0" class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400" @click="confirmColumnSelection">Auswahl bestätigen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data Preview -->
|
|
<div v-if="csvData.length > 0 && columnsSelected" class="bg-white rounded-xl shadow-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-xl font-semibold text-gray-900">Datenvorschau</h3>
|
|
<div class="flex space-x-2">
|
|
<button class="px-3 py-1 text-sm bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors" @click="exportCSV">CSV exportieren</button>
|
|
<button class="px-3 py-1 text-sm bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors" @click="clearData">Daten löschen</button>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50"><tr><th v-for="(header, index) in (columnsSelected ? filteredCsvHeaders : csvHeaders)" :key="index" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ header }}</th></tr></thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr v-for="(row, rowIndex) in (columnsSelected ? filteredCsvData : csvData).slice(0, 10)" :key="rowIndex" :class="rowIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'">
|
|
<td v-for="(cell, cellIndex) in row" :key="cellIndex" class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ cell }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="(columnsSelected ? filteredCsvData : csvData).length > 10" class="mt-4 text-center text-sm text-gray-600">Zeige erste 10 von {{ (columnsSelected ? filteredCsvData : csvData).length }} Zeilen</div>
|
|
<div class="mt-4 text-sm text-gray-600"><p><strong>Zeilen:</strong> {{ (columnsSelected ? filteredCsvData : csvData).length }}</p><p><strong>Spalten:</strong> {{ (columnsSelected ? filteredCsvHeaders : csvHeaders).length }}</p></div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-if="csvData.length === 0" class="text-center py-12 bg-white rounded-xl shadow-lg">
|
|
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
|
<p class="text-gray-600">Keine CSV-Daten geladen.</p>
|
|
<p class="text-sm text-gray-500 mt-2">Laden Sie eine CSV-Datei hoch, um Spielplandaten zu verwalten.</p>
|
|
</div>
|
|
|
|
<!-- Upload Modal -->
|
|
<div v-if="showUploadModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" @click.self="closeUploadModal">
|
|
<div class="bg-white rounded-lg max-w-md w-full p-6">
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">CSV-Datei hochladen</h3>
|
|
<div class="space-y-4">
|
|
<div><label class="block text-sm font-medium text-gray-700 mb-2">Datei auswählen</label><input ref="modalFileInput" type="file" accept=".csv" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" @change="handleModalFileSelect"></div>
|
|
<div v-if="selectedFile" class="p-3 bg-gray-50 rounded-lg"><p class="text-sm text-gray-700"><strong>Ausgewählte Datei:</strong> {{ selectedFile.name }}</p><p class="text-xs text-gray-500">{{ selectedFile.size }} bytes</p></div>
|
|
<div class="bg-blue-50 p-4 rounded-lg"><h4 class="text-sm font-medium text-blue-800 mb-2">Erwartetes CSV-Format:</h4><div class="text-xs text-blue-700 space-y-1"><p>• Erste Zeile: Spaltenüberschriften</p><p>• Trennzeichen: Komma (,)</p></div></div>
|
|
</div>
|
|
<div class="flex justify-end space-x-3 pt-4">
|
|
<button class="px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors" @click="closeUploadModal">Abbrechen</button>
|
|
<button :disabled="!selectedFile" class="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:bg-gray-400" @click="processSelectedFile">Hochladen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Processing Modal -->
|
|
<div v-if="isProcessing" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div class="bg-white rounded-lg max-w-sm w-full p-6 text-center">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto mb-4" />
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">Verarbeitung läuft...</h3>
|
|
<p class="text-sm text-gray-600">{{ processingMessage }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, computed } from 'vue'
|
|
|
|
const fileInput = ref(null)
|
|
const modalFileInput = ref(null)
|
|
const showUploadModal = ref(false)
|
|
const isProcessing = ref(false)
|
|
const processingMessage = ref('')
|
|
const isDragOver = ref(false)
|
|
const currentFile = ref(null)
|
|
const selectedFile = ref(null)
|
|
const csvData = ref([])
|
|
const csvHeaders = ref([])
|
|
const selectedColumns = ref([])
|
|
const columnsSelected = ref(false)
|
|
const filteredCsvData = ref([])
|
|
const filteredCsvHeaders = ref([])
|
|
|
|
const triggerFileInput = () => { fileInput.value?.click() }
|
|
const handleFileSelect = (event) => { const file = event.target.files[0]; if (file) processFile(file) }
|
|
const handleModalFileSelect = (event) => { selectedFile.value = event.target.files[0] }
|
|
const handleFileDrop = (event) => { isDragOver.value = false; const file = event.dataTransfer.files[0]; if (file && file.type === 'text/csv') processFile(file) }
|
|
|
|
const processFile = async (file) => {
|
|
isProcessing.value = true; processingMessage.value = 'Datei wird gelesen...'
|
|
try {
|
|
const text = await file.text(); processingMessage.value = 'CSV wird geparst...'
|
|
const lines = text.split('\n').filter(line => line.trim() !== '')
|
|
if (lines.length < 2) throw new Error('CSV-Datei muss mindestens eine Kopfzeile und eine Datenzeile enthalten')
|
|
const parseCSVLine = (line) => { const tabCount = (line.match(/\t/g) || []).length; const semicolonCount = (line.match(/;/g) || []).length; const delimiter = tabCount > semicolonCount ? '\t' : ';'; return line.split(delimiter).map(value => value.trim()) }
|
|
csvHeaders.value = parseCSVLine(lines[0]); csvData.value = lines.slice(1).map(line => parseCSVLine(line))
|
|
selectedColumns.value = new Array(csvHeaders.value.length).fill(true); columnsSelected.value = false
|
|
currentFile.value = { name: file.name, size: file.size, lastModified: file.lastModified }
|
|
processingMessage.value = 'Verarbeitung abgeschlossen!'
|
|
setTimeout(() => { isProcessing.value = false; showUploadModal.value = false }, 1000)
|
|
} catch (error) { console.error('Fehler:', error); alert('Fehler: ' + error.message); isProcessing.value = false }
|
|
}
|
|
|
|
const processSelectedFile = () => { if (selectedFile.value) processFile(selectedFile.value) }
|
|
const removeFile = () => { currentFile.value = null; csvData.value = []; csvHeaders.value = []; selectedColumns.value = []; columnsSelected.value = false; filteredCsvData.value = []; filteredCsvHeaders.value = []; if (fileInput.value) fileInput.value.value = '' }
|
|
const selectedColumnsCount = computed(() => selectedColumns.value.filter(s => s).length)
|
|
const getColumnPreview = (index) => { if (csvData.value.length === 0) return 'Keine Daten'; const sv = csvData.value.slice(0, 3).map(row => row[index]).filter(val => val && val.trim() !== ''); return sv.length > 0 ? `Beispiel: ${sv.join(', ')}` : 'Leere Spalte' }
|
|
const selectAllColumns = () => { selectedColumns.value = selectedColumns.value.map(() => true) }
|
|
const deselectAllColumns = () => { selectedColumns.value = selectedColumns.value.map(() => false) }
|
|
const confirmColumnSelection = () => { const si = selectedColumns.value.map((s, i) => s ? i : -1).filter(i => i !== -1); filteredCsvHeaders.value = si.map(i => csvHeaders.value[i]); filteredCsvData.value = csvData.value.map(row => si.map(i => row[i])); columnsSelected.value = true }
|
|
const suggestHalleColumns = () => { csvHeaders.value.forEach((header, index) => { const h = header.toLowerCase(); if (h.includes('halle') || h.includes('strasse') || h.includes('plz') || h.includes('ort')) selectedColumns.value[index] = true }) }
|
|
const clearData = () => { if (confirm('Möchten Sie alle Daten wirklich löschen?')) removeFile() }
|
|
|
|
const exportCSV = () => {
|
|
const d = columnsSelected.value ? filteredCsvData.value : csvData.value; const h = columnsSelected.value ? filteredCsvHeaders.value : csvHeaders.value
|
|
if (d.length === 0) return
|
|
const csv = [h.join(';'), ...d.map(row => row.join(';'))].join('\n')
|
|
const blob = new Blob([csv], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `spielplan_export_${new Date().toISOString().split('T')[0]}.csv`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a)
|
|
}
|
|
|
|
const save = async () => {
|
|
const d = columnsSelected.value ? filteredCsvData.value : csvData.value; const h = columnsSelected.value ? filteredCsvHeaders.value : csvHeaders.value
|
|
if (d.length === 0) { alert('Keine Daten zum Speichern vorhanden.'); return }
|
|
try {
|
|
const csv = [h.join(';'), ...d.map(row => row.join(';'))].join('\n')
|
|
const response = await fetch('/api/cms/save-csv', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: 'spielplan.csv', content: csv }) })
|
|
if (response.ok) alert('Spielplan erfolgreich gespeichert!'); else alert('Fehler beim Speichern!')
|
|
} catch (error) { console.error('Fehler:', error); alert('Fehler beim Speichern!') }
|
|
}
|
|
|
|
const closeUploadModal = () => { showUploadModal.value = false; selectedFile.value = null; if (modalFileInput.value) modalFileInput.value.value = '' }
|
|
|
|
onMounted(() => {
|
|
(async () => {
|
|
try {
|
|
const response = await fetch('/data/spielplan.csv'); if (!response.ok) return; const text = await response.text()
|
|
const lines = text.split('\n').filter(line => line.trim() !== ''); if (lines.length < 2) return
|
|
const parseCSVLine = (line) => { const values = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { inQuotes = !inQuotes } else if (char === ',' && !inQuotes) { values.push(current.trim()); current = '' } else { current += char } }; values.push(current.trim()); return values }
|
|
csvHeaders.value = parseCSVLine(lines[0]); csvData.value = lines.slice(1).map(line => parseCSVLine(line))
|
|
currentFile.value = { name: 'spielplan.csv', size: text.length, lastModified: null }
|
|
} catch { /* ignore */ }
|
|
})()
|
|
})
|
|
</script>
|