Enhance news component functionality and UI; implement dynamic grid layout in PublicNews.vue, add visibility and expiration options in news management, and update API to handle new fields for improved news filtering and display.
This commit is contained in:
@@ -11,11 +11,12 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div class="flex justify-center">
|
||||||
|
<div class="grid gap-8" :class="getGridClass()">
|
||||||
<article
|
<article
|
||||||
v-for="item in news"
|
v-for="item in news"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="bg-gray-50 rounded-xl p-6 border border-gray-200 hover:shadow-lg transition-shadow"
|
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"
|
||||||
>
|
>
|
||||||
<div class="flex items-center text-sm text-gray-500 mb-3">
|
<div class="flex items-center text-sm text-gray-500 mb-3">
|
||||||
<Calendar :size="16" class="mr-2" />
|
<Calendar :size="16" class="mr-2" />
|
||||||
@@ -26,12 +27,13 @@
|
|||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p class="text-gray-700 line-clamp-3">
|
<p class="text-gray-700 line-clamp-3 flex-grow">
|
||||||
{{ item.content }}
|
{{ item.content }}
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -60,6 +62,21 @@ const formatDate = (dateString) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadNews()
|
loadNews()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -43,6 +43,20 @@
|
|||||||
<Globe :size="14" class="mr-1" />
|
<Globe :size="14" class="mr-1" />
|
||||||
Öffentlich
|
Öffentlich
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="item.isHidden"
|
||||||
|
class="px-3 py-1 bg-yellow-100 text-yellow-800 text-xs font-semibold rounded-full flex items-center"
|
||||||
|
>
|
||||||
|
<EyeOff :size="14" class="mr-1" />
|
||||||
|
Ausgeblendet
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="item.expiresAt && isExpired(item.expiresAt)"
|
||||||
|
class="px-3 py-1 bg-red-100 text-red-800 text-xs font-semibold rounded-full flex items-center"
|
||||||
|
>
|
||||||
|
<Calendar :size="14" class="mr-1" />
|
||||||
|
Abgelaufen
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center text-sm text-gray-500 space-x-4">
|
<div class="flex items-center text-sm text-gray-500 space-x-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -145,6 +159,40 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="formData.isPublic" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Ablaufdatum (optional)</label>
|
||||||
|
<input
|
||||||
|
v-model="formData.expiresAt"
|
||||||
|
type="datetime-local"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||||
|
:disabled="isSaving"
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-gray-600 mt-1">
|
||||||
|
Nach diesem Datum wird die News automatisch nicht mehr auf der Startseite angezeigt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-3 p-4 bg-yellow-50 rounded-lg border border-yellow-200">
|
||||||
|
<input
|
||||||
|
id="isHidden"
|
||||||
|
v-model="formData.isHidden"
|
||||||
|
type="checkbox"
|
||||||
|
class="w-5 h-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
|
||||||
|
:disabled="isSaving"
|
||||||
|
/>
|
||||||
|
<label for="isHidden" class="text-sm font-medium text-gray-900 cursor-pointer flex-1">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<EyeOff :size="18" class="mr-2 text-yellow-600" />
|
||||||
|
<span>News ausblenden</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-600 mt-1 ml-6">
|
||||||
|
News wird nicht auf der Startseite angezeigt, bleibt aber im internen Bereich sichtbar.
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="errorMessage" class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm">
|
<div v-if="errorMessage" class="flex items-center p-3 rounded-md bg-red-50 text-red-700 text-sm">
|
||||||
<AlertCircle :size="20" class="mr-2" />
|
<AlertCircle :size="20" class="mr-2" />
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
@@ -177,7 +225,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { Newspaper, Plus, User, Calendar, Edit, Trash2, Loader2, AlertCircle, Globe } from 'lucide-vue-next'
|
import { Newspaper, Plus, User, Calendar, Edit, Trash2, Loader2, AlertCircle, Globe, EyeOff } from 'lucide-vue-next'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
@@ -191,7 +239,9 @@ const errorMessage = ref('')
|
|||||||
const formData = ref({
|
const formData = ref({
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
isPublic: false
|
isPublic: false,
|
||||||
|
expiresAt: '',
|
||||||
|
isHidden: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const canWrite = computed(() => {
|
const canWrite = computed(() => {
|
||||||
@@ -215,7 +265,9 @@ const openAddModal = () => {
|
|||||||
formData.value = {
|
formData.value = {
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
isPublic: false
|
isPublic: false,
|
||||||
|
expiresAt: '',
|
||||||
|
isHidden: false
|
||||||
}
|
}
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
@@ -226,12 +278,30 @@ const openEditModal = (item) => {
|
|||||||
formData.value = {
|
formData.value = {
|
||||||
title: item.title,
|
title: item.title,
|
||||||
content: item.content,
|
content: item.content,
|
||||||
isPublic: item.isPublic || false
|
isPublic: item.isPublic || false,
|
||||||
|
expiresAt: item.expiresAt ? convertUTCToLocal(item.expiresAt) : '',
|
||||||
|
isHidden: item.isHidden || false
|
||||||
}
|
}
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert UTC ISO string to datetime-local format
|
||||||
|
const convertUTCToLocal = (utcDateTime) => {
|
||||||
|
// UTC ISO format: "2025-10-24T12:39:00.000Z"
|
||||||
|
// Convert to local datetime-local format: "2025-10-24T12:39"
|
||||||
|
const utcDate = new Date(utcDateTime)
|
||||||
|
|
||||||
|
// Get local date components
|
||||||
|
const year = utcDate.getFullYear()
|
||||||
|
const month = String(utcDate.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(utcDate.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(utcDate.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(utcDate.getMinutes()).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
showModal.value = false
|
showModal.value = false
|
||||||
editingNews.value = null
|
editingNews.value = null
|
||||||
@@ -243,12 +313,19 @@ const saveNews = async () => {
|
|||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Convert datetime-local to ISO string (local time to UTC)
|
||||||
|
const dataToSend = {
|
||||||
|
id: editingNews.value?.id,
|
||||||
|
title: formData.value.title,
|
||||||
|
content: formData.value.content,
|
||||||
|
isPublic: formData.value.isPublic,
|
||||||
|
isHidden: formData.value.isHidden,
|
||||||
|
expiresAt: formData.value.expiresAt ? convertLocalToUTC(formData.value.expiresAt) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
await $fetch('/api/news', {
|
await $fetch('/api/news', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: dataToSend
|
||||||
id: editingNews.value?.id,
|
|
||||||
...formData.value
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
closeModal()
|
closeModal()
|
||||||
@@ -260,10 +337,17 @@ const saveNews = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmDelete = async (item) => {
|
// Convert datetime-local to UTC ISO string
|
||||||
console.log('Delete item:', item)
|
const convertLocalToUTC = (localDateTime) => {
|
||||||
console.log('Delete item.id:', item.id)
|
// datetime-local format: "2025-10-24T12:39"
|
||||||
|
// Create Date object in local timezone
|
||||||
|
const localDate = new Date(localDateTime)
|
||||||
|
|
||||||
|
// Convert to UTC ISO string
|
||||||
|
return localDate.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async (item) => {
|
||||||
window.showConfirmModal('News löschen', `Möchten Sie die News "${item.title}" wirklich löschen?`, async () => {
|
window.showConfirmModal('News löschen', `Möchten Sie die News "${item.title}" wirklich löschen?`, async () => {
|
||||||
if (!item.id) {
|
if (!item.id) {
|
||||||
window.showErrorModal('Fehler', 'News-ID fehlt!')
|
window.showErrorModal('Fehler', 'News-ID fehlt!')
|
||||||
@@ -271,7 +355,6 @@ const confirmDelete = async (item) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Deleting with ID:', item.id)
|
|
||||||
await $fetch(`/api/news?id=${encodeURIComponent(item.id)}`, {
|
await $fetch(`/api/news?id=${encodeURIComponent(item.id)}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
@@ -279,7 +362,6 @@ const confirmDelete = async (item) => {
|
|||||||
await loadNews()
|
await loadNews()
|
||||||
window.showSuccessModal('Erfolg', 'News wurde erfolgreich gelöscht')
|
window.showSuccessModal('Erfolg', 'News wurde erfolgreich gelöscht')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete error:', error)
|
|
||||||
window.showErrorModal('Fehler', 'Fehler beim Löschen der News: ' + (error.data?.message || error.message))
|
window.showErrorModal('Fehler', 'Fehler beim Löschen der News: ' + (error.data?.message || error.message))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -297,6 +379,11 @@ const formatDate = (dateString) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isExpired = (expiresAt) => {
|
||||||
|
if (!expiresAt) return false
|
||||||
|
return new Date(expiresAt) <= new Date()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadNews()
|
loadNews()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,9 +3,24 @@ import { readNews } from '../utils/news.js'
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
try {
|
try {
|
||||||
const allNews = await readNews()
|
const allNews = await readNews()
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
// Filter only public news
|
// Filter only public news that are not hidden and not expired
|
||||||
const publicNews = allNews.filter(item => item.isPublic === true)
|
const publicNews = allNews.filter(item => {
|
||||||
|
// Must be public
|
||||||
|
if (!item.isPublic) return false
|
||||||
|
|
||||||
|
// Must not be hidden
|
||||||
|
if (item.isHidden) return false
|
||||||
|
|
||||||
|
// Must not be expired
|
||||||
|
if (item.expiresAt) {
|
||||||
|
const expiresAt = new Date(item.expiresAt)
|
||||||
|
if (expiresAt <= now) return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
// Sort by created date, newest first
|
// Sort by created date, newest first
|
||||||
publicNews.sort((a, b) => new Date(b.created) - new Date(a.created))
|
publicNews.sort((a, b) => new Date(b.created) - new Date(a.created))
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
const { id, title, content, isPublic } = body
|
const { id, title, content, isPublic, expiresAt, isHidden } = body
|
||||||
|
|
||||||
if (!title || !content) {
|
if (!title || !content) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@@ -46,6 +46,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
isPublic: isPublic || false,
|
isPublic: isPublic || false,
|
||||||
|
expiresAt: expiresAt || undefined,
|
||||||
|
isHidden: isHidden || false,
|
||||||
author: user.name
|
author: user.name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"id": "da5490ca-4081-41ae-a652-22b7abeefc47",
|
||||||
|
"title": "test-news",
|
||||||
|
"content": "blablabla",
|
||||||
|
"isPublic": true,
|
||||||
|
"author": "Admin",
|
||||||
|
"created": "2025-10-24T10:30:21.559Z",
|
||||||
|
"updated": "2025-10-24T10:47:17.615Z",
|
||||||
|
"isHidden": false,
|
||||||
|
"expiresAt": "2025-10-24T10:50:00.000Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "024ef8e3-8ffc-4617-87aa-672189e0a41e",
|
"id": "024ef8e3-8ffc-4617-87aa-672189e0a41e",
|
||||||
"title": "Wir starten durch!",
|
"title": "Wir starten durch!",
|
||||||
|
|||||||
Reference in New Issue
Block a user