refactor(cleanup): remove deprecated block components

The ModerationBlock, SermonBlock, and SongsBlock Vue components are no longer
imported or used in the Edit.vue page after Task 12 restructuring. Removed
deprecated files, keeping ArrangementConfigurator which is still used by
SongEditModal and ArrangementDialog.
This commit is contained in:
Thorsten Bus 2026-03-29 12:28:58 +02:00
parent 6964931286
commit 665212fbea
3 changed files with 0 additions and 802 deletions

View file

@ -1,147 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
import { router } from '@inertiajs/vue3'
import axios from 'axios'
import SlideUploader from '@/Components/SlideUploader.vue'
import SlideGrid from '@/Components/SlideGrid.vue'
import ConfirmDialog from '@/Components/ConfirmDialog.vue'
const props = defineProps({
serviceId: {
type: Number,
required: true,
},
slides: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['slides-updated'])
// Filter slides to only show moderation slides for this service
const moderationSlides = computed(() => {
return props.slides.filter(
(slide) => slide.type === 'moderation' && slide.service_id === props.serviceId
)
})
// Delete all state (#2)
const confirmingDeleteAll = ref(false)
const deletingAll = ref(false)
function handleSlideUploaded() {
emit('slides-updated')
}
function handleSlideDeleted() {
emit('slides-updated')
}
function handleSlideUpdated() {
emit('slides-updated')
}
function confirmDeleteAll() {
deletingAll.value = true
axios.delete(route('slides.bulk-destroy'), {
data: { type: 'moderation', service_id: props.serviceId },
})
.then(() => {
confirmingDeleteAll.value = false
deletingAll.value = false
emit('slides-updated')
router.reload({ preserveScroll: true })
})
.catch(() => {
deletingAll.value = false
})
}
function downloadBundle() {
window.location.href = route('services.download-bundle', {
service: props.serviceId,
blockType: 'moderation',
})
}
</script>
<template>
<div data-testid="moderation-block" class="moderation-block space-y-4">
<!-- Block header with badge and delete-all (#2) -->
<div class="flex items-center justify-end gap-2">
<span
v-if="moderationSlides.length > 0"
class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-1 text-xs font-semibold text-gray-700 ring-1 ring-gray-200/80"
>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5a2.25 2.25 0 002.25-2.25V5.25a2.25 2.25 0 00-2.25-2.25H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z" />
</svg>
{{ moderationSlides.length }} {{ moderationSlides.length === 1 ? 'Folie' : 'Folien' }}
</span>
<!-- Delete all button (#2) -->
<button
v-if="moderationSlides.length > 0"
@click="confirmingDeleteAll = true"
class="flex h-7 w-7 items-center justify-center rounded-lg text-gray-400 transition hover:bg-red-50 hover:text-red-600"
title="Alle Folien l&ouml;schen"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</div>
<SlideGrid
data-testid="moderation-block-grid"
:slides="moderationSlides"
type="moderation"
:show-expire-date="false"
:show-uploader="true"
@deleted="handleSlideDeleted"
@updated="handleSlideUpdated"
>
<template #upload-card>
<SlideUploader
data-testid="moderation-block-uploader"
type="moderation"
:service-id="serviceId"
:show-expire-date="false"
:inline="true"
@uploaded="handleSlideUploaded"
/>
</template>
</SlideGrid>
<button
v-if="moderationSlides.length > 0"
@click="downloadBundle"
class="inline-flex items-center rounded-md border border-transparent bg-gray-600 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 active:bg-gray-900"
data-testid="download-probundle-button-moderation"
>
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
.probundle herunterladen
</button>
<!-- Delete all confirmation (#2) -->
<ConfirmDialog
:show="confirmingDeleteAll"
title="Alle Folien l&ouml;schen?"
message="M&ouml;chtest du wirklich alle Moderationsfolien l&ouml;schen?"
confirm-label="Alle l&ouml;schen"
cancel-label="Abbrechen"
variant="danger"
@confirm="confirmDeleteAll"
@cancel="confirmingDeleteAll = false"
/>
</div>
</template>
<style scoped>
.moderation-block {
/* Component-specific styles if needed */
}
</style>

View file

@ -1,147 +0,0 @@
<script setup>
import { ref, computed } from 'vue'
import { router } from '@inertiajs/vue3'
import axios from 'axios'
import SlideUploader from '@/Components/SlideUploader.vue'
import SlideGrid from '@/Components/SlideGrid.vue'
import ConfirmDialog from '@/Components/ConfirmDialog.vue'
const props = defineProps({
serviceId: {
type: Number,
required: true,
},
slides: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['slides-updated'])
// Filter slides to only show sermon slides for this service
const sermonSlides = computed(() => {
return props.slides.filter(
(slide) => slide.type === 'sermon' && slide.service_id === props.serviceId
)
})
// Delete all state (#2)
const confirmingDeleteAll = ref(false)
const deletingAll = ref(false)
function handleSlideUploaded() {
emit('slides-updated')
}
function handleSlideDeleted() {
emit('slides-updated')
}
function handleSlideUpdated() {
emit('slides-updated')
}
function confirmDeleteAll() {
deletingAll.value = true
axios.delete(route('slides.bulk-destroy'), {
data: { type: 'sermon', service_id: props.serviceId },
})
.then(() => {
confirmingDeleteAll.value = false
deletingAll.value = false
emit('slides-updated')
router.reload({ preserveScroll: true })
})
.catch(() => {
deletingAll.value = false
})
}
function downloadBundle() {
window.location.href = route('services.download-bundle', {
service: props.serviceId,
blockType: 'sermon',
})
}
</script>
<template>
<div data-testid="sermon-block" class="sermon-block space-y-4">
<!-- Block header with badge and delete-all (#2) -->
<div class="flex items-center justify-end gap-2">
<span
v-if="sermonSlides.length > 0"
class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-1 text-xs font-semibold text-gray-700 ring-1 ring-gray-200/80"
>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5a2.25 2.25 0 002.25-2.25V5.25a2.25 2.25 0 00-2.25-2.25H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z" />
</svg>
{{ sermonSlides.length }} {{ sermonSlides.length === 1 ? 'Folie' : 'Folien' }}
</span>
<!-- Delete all button (#2) -->
<button
v-if="sermonSlides.length > 0"
@click="confirmingDeleteAll = true"
class="flex h-7 w-7 items-center justify-center rounded-lg text-gray-400 transition hover:bg-red-50 hover:text-red-600"
title="Alle Folien l&ouml;schen"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</div>
<SlideGrid
data-testid="sermon-block-grid"
:slides="sermonSlides"
type="sermon"
:show-expire-date="false"
:show-uploader="true"
@deleted="handleSlideDeleted"
@updated="handleSlideUpdated"
>
<template #upload-card>
<SlideUploader
data-testid="sermon-block-uploader"
type="sermon"
:service-id="serviceId"
:show-expire-date="false"
:inline="true"
@uploaded="handleSlideUploaded"
/>
</template>
</SlideGrid>
<button
v-if="sermonSlides.length > 0"
@click="downloadBundle"
class="inline-flex items-center rounded-md border border-transparent bg-gray-600 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 active:bg-gray-900"
data-testid="download-probundle-button-sermon"
>
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
.probundle herunterladen
</button>
<!-- Delete all confirmation (#2) -->
<ConfirmDialog
:show="confirmingDeleteAll"
title="Alle Folien l&ouml;schen?"
message="M&ouml;chtest du wirklich alle Predigtfolien l&ouml;schen?"
confirm-label="Alle l&ouml;schen"
cancel-label="Abbrechen"
variant="danger"
@confirm="confirmDeleteAll"
@cancel="confirmingDeleteAll = false"
/>
</div>
</template>
<style scoped>
.sermon-block {
/* Component-specific styles if needed */
}
</style>

View file

@ -1,508 +0,0 @@
<script setup>
import ArrangementConfigurator from '@/Components/ArrangementConfigurator.vue'
import { router } from '@inertiajs/vue3'
import { reactive, ref, watch } from 'vue'
const props = defineProps({
serviceSongs: {
type: Array,
default: () => [],
},
songsCatalog: {
type: Array,
default: () => [],
},
})
const toastMessage = ref('')
const toastVariant = ref('info')
const selectedSongIds = reactive({})
const searchTerms = reactive({})
const translationValues = reactive({})
const dropdownOpen = reactive({})
const showToast = (message, variant = 'info') => {
toastMessage.value = message
toastVariant.value = variant
setTimeout(() => {
toastMessage.value = ''
}, 2500)
}
const normalize = (value) => (value ?? '').toString().toLowerCase().trim()
const initLocalState = () => {
props.serviceSongs.forEach((serviceSong) => {
selectedSongIds[serviceSong.id] = selectedSongIds[serviceSong.id] ?? ''
searchTerms[serviceSong.id] = searchTerms[serviceSong.id] ?? ''
translationValues[serviceSong.id] = serviceSong.use_translation
})
}
watch(
() => props.serviceSongs,
() => {
initLocalState()
},
{ immediate: true, deep: true },
)
const sortedSongs = () => {
return [...props.serviceSongs].sort((a, b) => a.order - b.order)
}
const filteredCatalog = (serviceSongId) => {
const term = normalize(searchTerms[serviceSongId])
if (term === '') {
return props.songsCatalog.slice(0, 100)
}
return props.songsCatalog
.filter((song) => normalize(song.title).includes(term) || normalize(song.ccli_id).includes(term))
.slice(0, 100)
}
const assignSong = async (serviceSongId) => {
const selectedSongId = Number(selectedSongIds[serviceSongId])
if (!selectedSongId) {
showToast('Bitte waehle zuerst einen Song aus.', 'warning')
return
}
try {
await window.axios.post(`/api/service-songs/${serviceSongId}/assign`, {
song_id: selectedSongId,
})
selectedSongIds[serviceSongId] = ''
searchTerms[serviceSongId] = ''
showToast('Song wurde zugeordnet.', 'success')
router.reload({
only: ['serviceSongs'],
preserveScroll: true,
preserveState: true,
})
} catch {
showToast('Zuordnung fehlgeschlagen.', 'error')
}
}
const requestCreation = async (serviceSongId) => {
try {
await window.axios.post(`/api/service-songs/${serviceSongId}/request`)
showToast('Anfrage wurde gesendet.', 'success')
router.reload({
only: ['serviceSongs'],
preserveScroll: true,
preserveState: true,
})
} catch {
showToast('Anfrage konnte nicht gesendet werden.', 'error')
}
}
const updateUseTranslation = async (serviceSongId) => {
try {
await window.axios.patch(`/api/service-songs/${serviceSongId}`, {
use_translation: Boolean(translationValues[serviceSongId]),
})
showToast('Uebersetzung wurde gespeichert.', 'success')
} catch {
showToast('Speichern fehlgeschlagen.', 'error')
}
}
const updateArrangementSelection = async (serviceSongId, arrangementId) => {
try {
await window.axios.patch(`/api/service-songs/${serviceSongId}`, {
song_arrangement_id: arrangementId,
})
showToast('Arrangement wurde gespeichert.', 'success')
} catch {
showToast('Arrangement konnte nicht gespeichert werden.', 'error')
}
}
const openDropdown = (serviceSongId) => {
dropdownOpen[serviceSongId] = true
}
const closeDropdown = (serviceSongId) => {
setTimeout(() => { dropdownOpen[serviceSongId] = false }, 200)
}
const selectSong = (serviceSongId, song) => {
selectedSongIds[serviceSongId] = song.id
searchTerms[serviceSongId] = song.title
dropdownOpen[serviceSongId] = false
}
// Preview / PDF
const previewData = ref(null)
const showPreview = ref(false)
const previewLoading = ref(false)
const openSongPreview = async (serviceSong) => {
if (!serviceSong.song_id || !serviceSong.song_arrangement_id) {
showToast('Bitte zuerst ein Arrangement auswählen.', 'warning')
return
}
previewLoading.value = true
showPreview.value = true
try {
const response = await window.axios.get(
`/songs/${serviceSong.song_id}/arrangements/${serviceSong.song_arrangement_id}/preview`
)
previewData.value = response.data
} catch {
showToast('Vorschau konnte nicht geladen werden.', 'error')
showPreview.value = false
} finally {
previewLoading.value = false
}
}
const closePreview = () => {
showPreview.value = false
previewData.value = null
}
const downloadPreviewPdf = () => {
if (previewData.value?.song?.id && previewData.value?.arrangement?.id) {
window.location.href = `/songs/${previewData.value.song.id}/arrangements/${previewData.value.arrangement.id}/pdf`
}
}
const downloadSongPdf = (serviceSong) => {
if (!serviceSong.song_id || !serviceSong.song_arrangement_id) {
showToast('Bitte zuerst ein Arrangement auswählen.', 'warning')
return
}
window.location.href = `/songs/${serviceSong.song_id}/arrangements/${serviceSong.song_arrangement_id}/pdf`
}
const formatDateTime = (value) => {
if (!value) {
return null
}
return new Date(value).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
const toastClasses = () => {
if (toastVariant.value === 'success') {
return 'border-emerald-300 bg-emerald-50 text-emerald-700'
}
if (toastVariant.value === 'warning') {
return 'border-amber-300 bg-amber-50 text-amber-700'
}
if (toastVariant.value === 'error') {
return 'border-red-300 bg-red-50 text-red-700'
}
return 'border-slate-300 bg-slate-50 text-slate-700'
}
</script>
<template>
<div data-testid="songs-block" class="space-y-4">
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-1 opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="toastMessage"
class="rounded-lg border px-4 py-3 text-sm font-medium"
:class="toastClasses()"
role="status"
>
{{ toastMessage }}
</div>
</Transition>
<div
data-testid="songs-block-song-card"
v-for="serviceSong in sortedSongs()"
:key="serviceSong.id"
class="space-y-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm"
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-gray-500">
Song {{ serviceSong.order }}
</p>
<h4 class="mt-1 text-lg font-semibold text-gray-900">
{{ serviceSong.cts_song_name || '-' }}
</h4>
<div class="mt-2 flex flex-wrap items-center gap-2 text-xs text-gray-600">
<span class="rounded-full bg-gray-100 px-2.5 py-1 font-medium">
CCLI: {{ serviceSong.song?.ccli_id || serviceSong.cts_ccli_id || '-' }}
</span>
<span class="rounded-full bg-gray-100 px-2.5 py-1 font-medium">
Hat Uebersetzung:
{{ serviceSong.song?.has_translation ? 'Ja' : 'Nein' }}
</span>
</div>
</div>
<span
v-if="serviceSong.song_id"
class="inline-flex items-center rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700"
>
Zugeordnet
</span>
<span
v-else
class="inline-flex items-center rounded-full bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-700"
>
Nicht zugeordnet
</span>
</div>
<div
v-if="!serviceSong.song_id"
class="space-y-4 rounded-lg border border-amber-200 bg-amber-50/40 p-4"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<p class="text-sm font-semibold text-amber-900">
Dieser CTS-Song ist noch nicht in der Song-DB verknuepft.
</p>
<p
v-if="serviceSong.request_sent_at"
class="mt-1 text-xs text-amber-700"
>
Anfrage gesendet am {{ formatDateTime(serviceSong.request_sent_at) }}
</p>
</div>
<button
data-testid="songs-block-request-button"
type="button"
class="inline-flex items-center rounded-md border border-amber-300 bg-white px-3 py-2 text-xs font-semibold text-amber-800 transition hover:bg-amber-100"
@click="requestCreation(serviceSong.id)"
>
Erstellung anfragen
</button>
</div>
<div class="grid gap-3 md:grid-cols-[1fr,auto] md:items-end">
<div class="relative">
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-gray-600">
Song suchen und auswählen
</label>
<input
data-testid="songs-block-search-input"
v-model="searchTerms[serviceSong.id]"
type="text"
placeholder="Titel oder CCLI eingeben…"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-emerald-500 focus:ring-emerald-500"
@focus="openDropdown(serviceSong.id)"
@input="openDropdown(serviceSong.id)"
@blur="closeDropdown(serviceSong.id)"
>
<!-- Selected indicator -->
<div v-if="selectedSongIds[serviceSong.id]" class="mt-1 flex items-center gap-2">
<span class="text-xs text-emerald-700 font-medium">
Ausgewählt: {{ songsCatalog.find(s => s.id === Number(selectedSongIds[serviceSong.id]))?.title || '' }}
</span>
<button type="button" class="text-xs text-gray-400 hover:text-red-500" @click="selectedSongIds[serviceSong.id] = ''; searchTerms[serviceSong.id] = ''">&#10005;</button>
</div>
<!-- Dropdown -->
<div
v-if="dropdownOpen[serviceSong.id] && filteredCatalog(serviceSong.id).length > 0"
class="absolute z-30 mt-1 max-h-48 w-full overflow-auto rounded-md border border-gray-200 bg-white shadow-lg"
>
<button
v-for="song in filteredCatalog(serviceSong.id)"
:key="song.id"
type="button"
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-emerald-50"
@mousedown.prevent="selectSong(serviceSong.id, song)"
>
<span class="font-medium text-gray-900">{{ song.title }}</span>
<span class="ml-auto text-xs text-gray-400">CCLI: {{ song.ccli_id || '' }}</span>
</button>
</div>
</div>
<button
data-testid="songs-block-assign-button"
type="button"
class="inline-flex items-center justify-center rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white shadow transition hover:bg-emerald-700"
@click="assignSong(serviceSong.id)"
>
Zuordnen
</button>
</div>
</div>
<div v-else class="space-y-4">
<div class="flex flex-wrap items-center gap-4">
<p class="text-sm text-gray-600">
Zugeordnet zu:
<span class="font-semibold text-gray-900">
{{ serviceSong.song?.title }}
</span>
</p>
<label
v-if="serviceSong.song?.has_translation"
class="inline-flex items-center gap-2 text-sm font-medium text-gray-800"
>
<input
data-testid="songs-block-translation-checkbox"
v-model="translationValues[serviceSong.id]"
type="checkbox"
class="rounded border-gray-300 text-emerald-600 focus:ring-emerald-500"
@change="updateUseTranslation(serviceSong.id)"
>
Uebersetzung verwenden
</label>
</div>
<ArrangementConfigurator
:song-id="serviceSong.song.id"
:arrangements="serviceSong.song.arrangements || []"
:available-groups="serviceSong.song.groups || []"
:selected-arrangement-id="serviceSong.song_arrangement_id"
@arrangement-selected="(arrangementId) => updateArrangementSelection(serviceSong.id, arrangementId)"
/>
<div class="flex flex-wrap gap-2">
<button
data-testid="songs-block-preview-button"
type="button"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-xs font-semibold text-gray-700 transition hover:bg-gray-50"
:disabled="!serviceSong.song_arrangement_id"
:class="{ 'opacity-50 cursor-not-allowed': !serviceSong.song_arrangement_id }"
@click="openSongPreview(serviceSong)"
>
Vorschau
</button>
<button
data-testid="songs-block-download-button"
type="button"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-xs font-semibold text-gray-700 transition hover:bg-gray-50"
:disabled="!serviceSong.song_arrangement_id"
:class="{ 'opacity-50 cursor-not-allowed': !serviceSong.song_arrangement_id }"
@click="downloadSongPdf(serviceSong)"
>
PDF herunterladen
</button>
<a
data-testid="songs-block-download-pro-button"
:href="`/api/songs/${serviceSong.song_id}/download-pro`"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-xs font-semibold text-gray-700 transition hover:bg-gray-50"
>
.pro herunterladen
</a>
</div>
</div>
</div>
<div
v-if="serviceSongs.length === 0"
class="rounded-xl border border-dashed border-gray-300 bg-gray-50 px-4 py-10 text-center"
>
<p class="text-sm font-medium text-gray-600">
Fuer diesen Service sind aktuell keine Songs vorhanden.
</p>
</div>
<!-- Song Preview Modal -->
<Teleport to="body">
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="showPreview"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"
@click.self="closePreview"
>
<div class="w-full max-w-2xl max-h-[80vh] overflow-auto rounded-2xl bg-white p-6 shadow-2xl">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">
{{ previewData?.song?.title || 'Vorschau' }}
</h3>
<button type="button" class="text-gray-400 hover:text-gray-600" @click="closePreview">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div v-if="previewLoading" class="flex items-center justify-center py-12">
<span class="inline-block h-6 w-6 animate-spin rounded-full border-2 border-amber-400 border-t-transparent"></span>
</div>
<div v-else-if="previewData" class="space-y-3">
<div v-for="(group, idx) in previewData.groups" :key="idx"
class="rounded-lg border overflow-hidden"
:style="{ borderColor: group.color || '#6b7280' }"
>
<!-- Group name as box title bar -->
<div class="px-3 py-1.5 text-xs font-bold text-white"
:style="{ backgroundColor: group.color || '#6b7280' }"
>
{{ group.name }}
</div>
<!-- Slides content with lightened background -->
<div class="px-3 py-2 space-y-1"
:style="{ backgroundColor: (group.color || '#6b7280') + '15' }"
>
<div v-for="(slide, sIdx) in group.slides" :key="sIdx"
class="whitespace-pre-wrap text-sm text-gray-700 leading-snug"
>{{ slide.text_content }}</div>
</div>
</div>
<div v-if="previewData.song?.copyright_text" class="border-t pt-3 text-xs text-gray-400">
{{ previewData.song.copyright_text }}
</div>
<div class="flex justify-end pt-3 border-t border-gray-200">
<button
type="button"
class="inline-flex items-center gap-1.5 rounded-lg bg-amber-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-700"
@click="downloadPreviewPdf"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
PDF herunterladen
</button>
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</div>
</template>