fix: news frontend

This commit is contained in:
DecDuck
2025-03-10 12:05:10 +11:00
parent b6f52f660a
commit 1eede0f035
7 changed files with 53 additions and 62 deletions
+4 -4
View File
@@ -37,10 +37,10 @@
</div> </div>
<div> <div>
<label for="excerpt" class="block text-sm font-medium text-zinc-400">Excerpt</label> <label for="excerpt" class="block text-sm font-medium text-zinc-400">Exercept</label>
<input <input
id="excerpt" id="excerpt"
v-model="newArticle.excerpt" v-model="newArticle.description"
type="text" type="text"
class="mt-1 block w-full rounded-md bg-zinc-900 border-zinc-700 text-zinc-100 shadow-sm focus:border-primary-500 focus:ring-primary-500" class="mt-1 block w-full rounded-md bg-zinc-900 border-zinc-700 text-zinc-100 shadow-sm focus:border-primary-500 focus:ring-primary-500"
required required
@@ -171,7 +171,7 @@ const newTagInput = ref('');
const newArticle = ref({ const newArticle = ref({
title: '', title: '',
excerpt: '', description: '',
content: '', content: '',
image: '', image: '',
tags: [] as string[] tags: [] as string[]
@@ -290,7 +290,7 @@ const createArticle = async () => {
// Reset form // Reset form
newArticle.value = { newArticle.value = {
title: '', title: '',
excerpt: '', description: '',
content: '', content: '',
image: '', image: '',
tags: [] tags: []
+4 -4
View File
@@ -87,7 +87,7 @@
</div> </div>
<h3 class="relative text-sm font-medium text-zinc-100">{{ article.title }}</h3> <h3 class="relative text-sm font-medium text-zinc-100">{{ article.title }}</h3>
<p class="relative mt-1 text-xs text-zinc-400 line-clamp-2" v-html="formatExcerpt(article.excerpt)"></p> <p class="relative mt-1 text-xs text-zinc-400 line-clamp-2" v-html="formatExcerpt(article.description)"></p>
<div class="relative mt-2 flex items-center gap-x-2 text-xs text-zinc-500"> <div class="relative mt-2 flex items-center gap-x-2 text-xs text-zinc-500">
<time :datetime="article.publishedAt">{{ formatDate(article.publishedAt) }}</time> <time :datetime="article.publishedAt">{{ formatDate(article.publishedAt) }}</time>
</div> </div>
@@ -115,7 +115,7 @@ const availableTags = computed(() => {
if (!articles.value) return []; if (!articles.value) return [];
const tags = new Set<string>(); const tags = new Set<string>();
articles.value.forEach(article => { articles.value.forEach(article => {
article.tags.forEach(tag => tags.add(tag)); article.tags.forEach(tag => tags.add(tag.name));
}); });
return Array.from(tags); return Array.from(tags);
}); });
@@ -151,7 +151,7 @@ const filteredArticles = computed(() => {
return articles.value.filter((article) => { return articles.value.filter((article) => {
const matchesSearch = const matchesSearch =
article.title.toLowerCase().includes(searchQuery.value.toLowerCase()) || article.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
article.excerpt.toLowerCase().includes(searchQuery.value.toLowerCase()); article.description.toLowerCase().includes(searchQuery.value.toLowerCase());
const articleDate = new Date(article.publishedAt); const articleDate = new Date(article.publishedAt);
const now = new Date(); const now = new Date();
@@ -175,7 +175,7 @@ const filteredArticles = computed(() => {
} }
const matchesTags = selectedTags.value.length === 0 || const matchesTags = selectedTags.value.length === 0 ||
selectedTags.value.every(tag => article.tags.includes(tag)); selectedTags.value.every(tag => article.tags.find((e) => e.name == tag));
return matchesSearch && matchesDate && matchesTags; return matchesSearch && matchesDate && matchesTags;
}); });
@@ -23,20 +23,20 @@ export const useNews = () => {
const create = async (article: { const create = async (article: {
title: string; title: string;
excerpt: string; description: string;
content: string; content: string;
image?: string; image?: string;
tags: string[]; tags: string[];
authorId: string; authorId: string;
}) => { }) => {
return await $fetch('/api/v1/news', { return await $fetch('/api/v1/admin/news', {
method: 'POST', method: 'POST',
body: article body: article
}); });
}; };
const remove = async (id: string) => { const remove = async (id: string) => {
return await $fetch(`/api/v1/news/${id}`, { return await $fetch(`/api/v1/admin/news/${id}`, {
method: 'DELETE' method: 'DELETE'
}); });
}; };
@@ -1,5 +1,5 @@
<template> <template>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div v-if="article" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Banner header with blurred background --> <!-- Banner header with blurred background -->
<div class="relative w-full h-[300px] mb-8 rounded-lg overflow-hidden"> <div class="relative w-full h-[300px] mb-8 rounded-lg overflow-hidden">
<div class="absolute inset-0"> <div class="absolute inset-0">
@@ -42,19 +42,19 @@
<div class="flex flex-col gap-y-3 sm:flex-row sm:items-center sm:gap-x-4 text-zinc-300"> <div class="flex flex-col gap-y-3 sm:flex-row sm:items-center sm:gap-x-4 text-zinc-300">
<div class="flex items-center gap-x-4"> <div class="flex items-center gap-x-4">
<time :datetime="article.publishedAt">{{ formatDate(article.publishedAt) }}</time> <time :datetime="article.publishedAt">{{ formatDate(article.publishedAt) }}</time>
<span class="text-blue-400">{{ article.author.displayName }}</span> <span class="text-blue-400">{{ article.author?.displayName ?? "System" }}</span>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<span <span
v-for="tag in article.tags" v-for="tag in article.tags"
:key="tag" :key="tag.id"
class="inline-flex items-center rounded-full bg-zinc-800/80 backdrop-blur-sm px-3 py-1 text-sm font-semibold text-zinc-100" class="inline-flex items-center rounded-full bg-zinc-800/80 backdrop-blur-sm px-3 py-1 text-sm font-semibold text-zinc-100"
> >
{{ tag }} {{ tag.name }}
</span> </span>
</div> </div>
</div> </div>
<p class="mt-4 text-lg text-zinc-300">{{ article.excerpt }}</p> <p class="mt-4 text-lg text-zinc-300">{{ article.description }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -88,7 +88,7 @@ if (!article.value) {
// Render markdown content // Render markdown content
const renderedContent = computed(() => { const renderedContent = computed(() => {
return micromark(article.value.content); return micromark(article.value?.content ?? "");
}); });
const formatDate = (date: string) => { const formatDate = (date: string) => {
+7 -21
View File
@@ -24,7 +24,7 @@
<NuxtLink <NuxtLink
v-for="article in articles" v-for="article in articles"
:key="article.id" :key="article.id"
:to="`/news/article/${article.id}`" :to="`/news/${article.id}`"
class="block" class="block"
> >
<article <article
@@ -39,10 +39,10 @@
<div class="absolute top-4 left-4 flex gap-2"> <div class="absolute top-4 left-4 flex gap-2">
<span <span
v-for="tag in article.tags" v-for="tag in article.tags"
:key="tag" :key="tag.id"
class="inline-flex items-center rounded-full bg-zinc-900/75 px-3 py-1 text-sm font-semibold text-zinc-100 backdrop-blur" class="inline-flex items-center rounded-full bg-zinc-900/75 px-3 py-1 text-sm font-semibold text-zinc-100 backdrop-blur"
> >
{{ tag }} {{ tag.name }}
</span> </span>
</div> </div>
</div> </div>
@@ -53,14 +53,14 @@
<time :datetime="article.publishedAt" class="text-sm text-zinc-400"> <time :datetime="article.publishedAt" class="text-sm text-zinc-400">
{{ formatDate(article.publishedAt) }} {{ formatDate(article.publishedAt) }}
</time> </time>
<span class="text-sm text-blue-400">{{ article.author.displayName }}</span> <span class="text-sm text-blue-400">{{ article.author?.displayName ?? "System" }}</span>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<h3 class="text-xl font-semibold text-zinc-100 group-hover:text-primary-400"> <h3 class="text-xl font-semibold text-zinc-100 group-hover:text-primary-400">
{{ article.title }} {{ article.title }}
</h3> </h3>
<p class="mt-3 text-base text-zinc-400"> <p class="mt-3 text-base text-zinc-400">
{{ article.excerpt }} {{ article.description }}
</p> </p>
</div> </div>
</div> </div>
@@ -70,7 +70,7 @@
</TransitionGroup> </TransitionGroup>
<div <div
v-if="articles.length === 0" v-if="articles?.length === 0"
class="text-center py-12" class="text-center py-12"
> >
<DocumentIcon class="mx-auto h-12 w-12 text-zinc-400" /> <DocumentIcon class="mx-auto h-12 w-12 text-zinc-400" />
@@ -83,25 +83,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { DocumentIcon } from "@heroicons/vue/24/outline"; import { DocumentIcon } from "@heroicons/vue/24/outline";
interface Article {
id: string;
title: string;
excerpt: string;
content: string;
tags: string[];
image: string | null;
publishedAt: string;
author: {
id: string;
displayName: string;
};
}
const newsDirectory = ref(); const newsDirectory = ref();
const { data: articles, refresh: refreshArticles } = await useNews().getAll(); const { data: articles, refresh: refreshArticles } = await useNews().getAll();
const formatDate = (date: string) => { const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("en-US", { return new Date(date).toLocaleDateString("en-AU", {
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
@@ -1,16 +1,15 @@
import { defineEventHandler, createError } from "h3"; import { defineEventHandler, createError } from "h3";
import aclManager from "~/server/internal/acls";
import newsManager from "~/server/internal/news"; import newsManager from "~/server/internal/news";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (h3) => {
const userId = await event.context.session.getUserId(event); const allowed = await aclManager.allowSystemACL(h3, ["news:delete"]);
if (!userId) { if (!allowed)
throw createError({ throw createError({
statusCode: 401, statusCode: 403,
message: "Unauthorized",
}); });
}
const id = event.context.params?.id; const id = h3.context.params?.id;
if (!id) { if (!id) {
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
+23 -17
View File
@@ -45,23 +45,27 @@ class NewsManager {
return await prisma.article.findMany({ return await prisma.article.findMany({
where: { where: {
AND: [ AND: [
{ options.tags
tags: { ? {
some: { OR: options.tags?.map((e) => ({ name: e })) ?? [] }, tags: {
}, some: { OR: options.tags?.map((e) => ({ name: e })) ?? [] },
}, },
{ }
title: { : undefined,
search: options.search options.search
}, ? {
description: { title: {
search: options.search search: options.search,
}, },
content: { description: {
search: options.search search: options.search,
} },
} content: {
], search: options.search,
},
}
: undefined,
].filter((e) => e !== undefined),
}, },
take: options?.take || 10, take: options?.take || 10,
skip: options?.skip || 0, skip: options?.skip || 0,
@@ -75,6 +79,7 @@ class NewsManager {
displayName: true, displayName: true,
}, },
}, },
tags: true,
}, },
}); });
} }
@@ -89,6 +94,7 @@ class NewsManager {
displayName: true, displayName: true,
}, },
}, },
tags: true,
}, },
}); });
} }