mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-06-22 04:11:32 +10:00
fix: news frontend
This commit is contained in:
@@ -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: []
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user