Files
harheimertc/components/PublicNews.vue
Torsten Schulz (local) bf1caefde4
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 7m31s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m1s
feat: update security headers and improve content security policy; enhance hero image component and loading states in public news
2026-05-31 14:19:15 +02:00

191 lines
5.4 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>
<!-- News Modal -->
<div
v-if="selectedNews"
class="fixed inset-0 z-50 bg-black bg-opacity-50 flex items-center justify-center p-4"
@click.self="closeNewsModal"
>
<div class="bg-white rounded-xl shadow-2xl max-w-4xl w-full max-h-[90vh] flex flex-col">
<!-- Modal Header -->
<div class="flex items-center justify-between p-6 border-b border-gray-200">
<div class="flex-1">
<div class="flex items-center text-sm text-gray-500 mb-2">
<Calendar
:size="16"
class="mr-2"
/>
{{ formatDate(selectedNews.created) }}
</div>
<h2 class="text-2xl font-display font-bold text-gray-900">
{{ selectedNews.title }}
</h2>
</div>
<button
class="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
@click="closeNewsModal"
>
<X :size="24" />
</button>
</div>
<!-- Modal Content (scrollable) -->
<div class="p-6 overflow-y-auto flex-1">
<div class="prose max-w-none text-gray-700 whitespace-pre-wrap">
{{ selectedNews.content }}
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Calendar, X } from 'lucide-vue-next'
const news = ref([])
const selectedNews = ref(null)
const isLoading = ref(true)
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
// Verhindere Scrollen im Hintergrund
document.body.style.overflow = 'hidden'
}
const closeNewsModal = () => {
selectedNews.value = null
// Erlaube Scrollen wieder
document.body.style.overflow = ''
}
onMounted(() => {
loadNews()
})
</script>
<style scoped>
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>