226 lines
6.7 KiB
Vue
226 lines
6.7 KiB
Vue
<template>
|
|
<section
|
|
class="py-16 sm:py-20 bg-white min-h-[32rem]"
|
|
>
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="text-center mb-16">
|
|
<h2 class="text-4xl sm:text-5xl font-display font-bold text-gray-900 mb-4">
|
|
Aktuelles
|
|
</h2>
|
|
<div class="w-24 h-1 bg-primary-600 mx-auto mb-6" />
|
|
<p class="text-xl text-gray-600 max-w-3xl mx-auto">
|
|
Die neuesten Nachrichten aus unserem Verein
|
|
</p>
|
|
</div>
|
|
|
|
<div
|
|
v-if="isLoading"
|
|
class="grid gap-8 grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
|
|
>
|
|
<div
|
|
v-for="placeholder in 3"
|
|
:key="`news-placeholder-${placeholder}`"
|
|
class="bg-gray-50 rounded-xl p-6 border border-gray-200"
|
|
>
|
|
<div class="h-4 w-32 bg-gray-200 rounded animate-pulse mb-4" />
|
|
<div class="h-7 w-3/4 bg-gray-200 rounded animate-pulse mb-4" />
|
|
<div class="space-y-2">
|
|
<div class="h-4 w-full bg-gray-200 rounded animate-pulse" />
|
|
<div class="h-4 w-5/6 bg-gray-200 rounded animate-pulse" />
|
|
<div class="h-4 w-2/3 bg-gray-200 rounded animate-pulse" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="news.length > 0"
|
|
class="flex justify-center"
|
|
>
|
|
<div
|
|
class="grid gap-8"
|
|
:class="getGridClass()"
|
|
>
|
|
<article
|
|
v-for="item in news"
|
|
:key="item.id"
|
|
class="bg-gray-50 rounded-xl p-6 border border-gray-200 hover:shadow-lg transition-shadow w-full max-w-sm flex flex-col cursor-pointer"
|
|
@click="openNewsModal(item)"
|
|
>
|
|
<div class="flex items-center text-sm text-gray-500 mb-3">
|
|
<Calendar
|
|
:size="16"
|
|
class="mr-2"
|
|
/>
|
|
{{ formatDate(item.created) }}
|
|
</div>
|
|
|
|
<h3 class="text-xl font-display font-bold text-gray-900 mb-3">
|
|
{{ item.title }}
|
|
</h3>
|
|
|
|
<p class="text-gray-700 line-clamp-3 flex-grow">
|
|
{{ item.content }}
|
|
</p>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
class="max-w-xl mx-auto text-center bg-gray-50 border border-gray-200 rounded-xl p-8"
|
|
>
|
|
<p class="text-gray-700 font-semibold mb-2">
|
|
Aktuell keine News
|
|
</p>
|
|
<p class="text-gray-600 text-sm">
|
|
Neue Vereinsnachrichten erscheinen hier automatisch.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Teleport to="body">
|
|
<Transition name="news-modal">
|
|
<div
|
|
v-if="selectedNews"
|
|
class="fixed inset-0 z-[100] flex items-center justify-center bg-slate-950/65 px-4 py-6 sm:px-6"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
:aria-labelledby="modalTitleId"
|
|
@click.self="closeNewsModal"
|
|
>
|
|
<article class="w-full max-w-3xl max-h-[min(44rem,calc(100vh-3rem))] overflow-hidden rounded-lg bg-white shadow-2xl ring-1 ring-black/10 flex flex-col">
|
|
<header class="flex items-start gap-4 border-b border-gray-200 bg-white px-5 py-4 sm:px-7 sm:py-6">
|
|
<div class="min-w-0 flex-1">
|
|
<div class="mb-3 inline-flex items-center gap-2 rounded-full bg-primary-50 px-3 py-1 text-sm font-medium text-primary-800">
|
|
<Calendar :size="15" />
|
|
<time :datetime="selectedNews.created">
|
|
{{ formatDate(selectedNews.created) }}
|
|
</time>
|
|
</div>
|
|
<h2
|
|
:id="modalTitleId"
|
|
class="text-2xl sm:text-3xl font-display font-bold leading-tight text-gray-950 break-words"
|
|
>
|
|
{{ selectedNews.title }}
|
|
</h2>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="shrink-0 rounded-md border border-gray-200 bg-white p-2 text-gray-500 shadow-sm transition-colors hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
aria-label="News schließen"
|
|
@click="closeNewsModal"
|
|
>
|
|
<X :size="20" />
|
|
</button>
|
|
</header>
|
|
|
|
<div class="overflow-y-auto px-5 py-5 sm:px-7 sm:py-6">
|
|
<div class="news-modal-content text-base leading-7 text-gray-800 sm:text-lg sm:leading-8">
|
|
{{ selectedNews.content }}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { Calendar, X } from 'lucide-vue-next'
|
|
|
|
const news = ref([])
|
|
const selectedNews = ref(null)
|
|
const isLoading = ref(true)
|
|
const modalTitleId = 'public-news-modal-title'
|
|
|
|
const loadNews = async () => {
|
|
try {
|
|
const response = await $fetch('/api/news-public')
|
|
news.value = Array.isArray(response?.news) ? response.news : []
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der öffentlichen News:', error)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return ''
|
|
const date = new Date(dateString)
|
|
return date.toLocaleDateString('de-DE', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})
|
|
}
|
|
|
|
const getGridClass = () => {
|
|
const count = news.value.length
|
|
|
|
if (count === 1) {
|
|
// Eine Kachel: Eine Spalte, zentriert
|
|
return 'grid-cols-1 place-items-center'
|
|
} else if (count === 2) {
|
|
// Zwei Kacheln: Zwei Spalten, zentriert, gleiche Höhe
|
|
return 'grid-cols-1 md:grid-cols-2 place-items-stretch'
|
|
} else {
|
|
// Drei oder mehr Kacheln: Normale Grid-Darstellung
|
|
return 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
|
|
}
|
|
}
|
|
|
|
const openNewsModal = (item) => {
|
|
selectedNews.value = item
|
|
document.body.style.overflow = 'hidden'
|
|
document.addEventListener('keydown', handleModalKeydown)
|
|
}
|
|
|
|
const closeNewsModal = () => {
|
|
selectedNews.value = null
|
|
document.body.style.overflow = ''
|
|
document.removeEventListener('keydown', handleModalKeydown)
|
|
}
|
|
|
|
const handleModalKeydown = (event) => {
|
|
if (event.key === 'Escape') {
|
|
closeNewsModal()
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadNews()
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
document.body.style.overflow = ''
|
|
document.removeEventListener('keydown', handleModalKeydown)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.line-clamp-3 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.news-modal-content {
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.news-modal-enter-active,
|
|
.news-modal-leave-active {
|
|
transition: opacity 160ms ease;
|
|
}
|
|
|
|
.news-modal-enter-from,
|
|
.news-modal-leave-to {
|
|
opacity: 0;
|
|
}
|
|
</style>
|
|
|