Files
harheimertc/components/PublicNews.vue
Torsten Schulz (local) 5da11d2e4d
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 7m50s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
Fix in news, first android notification service
2026-06-10 13:47:33 +02:00

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>