final frontend

This commit is contained in:
Edd
2026-01-23 20:04:46 +00:00
parent 3838f3ff7a
commit e38613ca6b
6 changed files with 466 additions and 563 deletions
+7 -253
View File
@@ -135,12 +135,12 @@
class="flex flex-col items-center gap-1 text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 p-2 rounded-lg h-fit">
<button class="hover:text-blue-500 transition-colors text-lg"
@click.stop="votePost(post, 1)"></button>
:class="{ 'text-orange-500': post.my_vote === 'UP' }" @click.stop="votePost(post, 1)"></button>
<span class="text-xs font-bold">{{ post.rating }}</span>
<button class="hover:text-red-500 transition-colors text-lg"
@click.stop="votePost(post, -1)"></button>
:class="{ 'text-blue-500': post.my_vote === 'DOWN' }" @click.stop="votePost(post, -1)"></button>
</div>
<div class="flex-1">
@@ -161,7 +161,7 @@
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button v-if="authStore.user.role != 'COLLABORATOR'"
<button v-if="authStore.user.id == post.submitter.id || authStore.user.role != 'COLLABORATOR'"
class="p-1 text-slate-400 hover:text-amber-600 hover:bg-amber-50 dark:hover:bg-amber-900/20 rounded-lg transition-all transform hover:scale-110 active:scale-95 cursor-pointer"
:title="post.hidden ? 'Unhide' : 'Hide'" @click.stop="toggleHide(post)">
<svg width="16px" height="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
@@ -171,8 +171,8 @@
<path
d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" />
<line v-if="post.hidden" x1="3" y1="3" x2="13" y2="13" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" />
<line v-if="post.hidden" x1="3" y1="3" x2="13" y2="13" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" />
</svg>
</button>
</div>
@@ -213,216 +213,11 @@
<div v-if="loading"
class="animate-spin h-6 w-6 border-2 border-blue-600 border-t-transparent rounded-full" />
</div>
</div>
<div v-if="selectedPost" class="animate-fade-in-up">
<button
class="mb-6 flex items-center gap-2 text-sm font-bold text-blue-600 dark:text-blue-400 hover:underline"
@click="selectedPost = null">
BACK TO FEED
</button>
<article class="bg-white dark:bg-gray-900 border dark:border-gray-800 rounded-2xl shadow-xl overflow-hidden">
<PostDetail v-if="selectedPost" :post="selectedPost" :api="api" :token="token" @back="selectedPost = null"
@vote="(dir) => votePost(selectedPost, dir)" @comment-added="() => selectedPost.commentCount++" />
<div
class="relative bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900 p-8 border-b dark:border-gray-800">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-4">
<span
class="bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider shadow-sm border border-blue-200 dark:border-blue-800">
{{ selectedPost.type.replace('_', ' ') }}
</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
{{ new Date(selectedPost.publicationDate).toLocaleDateString('en-US', {
year:
'numeric', month: 'long', day: 'numeric'
}) }}
</span>
</div>
<h1 class="text-3xl md:text-4xl font-extrabold text-gray-900 dark:text-white mb-6 leading-tight">
{{ selectedPost.title }}
</h1>
<div
class="grid grid-cols-1 md:grid-cols-2 gap-6 p-4 bg-white/50 dark:bg-gray-800/50 rounded-xl border border-gray-200 dark:border-gray-700 backdrop-blur-sm">
<div>
<h4 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Authors
</h4>
<div class="flex flex-wrap gap-2">
<span v-for="(author, idx) in selectedPost.authors" :key="idx"
class="inline-flex items-center gap-1.5 text-sm font-semibold text-gray-700 dark:text-gray-200">
<span
class="w-6 h-6 rounded-full bg-indigo-100 text-indigo-600 dark:bg-indigo-900 dark:text-indigo-300 flex items-center justify-center text-xs">
{{ author.charAt(0) }}
</span>
{{ author }}
</span>
</div>
</div>
<div>
<h4 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Submitted
By</h4>
<div class="flex items-center gap-3">
<div
class="w-8 h-8 rounded-full bg-blue-600 text-white flex items-center justify-center font-bold text-xs">
{{ selectedPost.submitter.name.charAt(0) }}
</div>
<div>
<div class="text-sm font-bold text-gray-900 dark:text-white">{{
selectedPost.submitter.name }}</div>
<div class="text-xs text-gray-500">{{ selectedPost.submitter.role }} {{
selectedPost.submitter.email }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="p-8">
<div class="mb-10">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
</svg>
Abstract
</h3>
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-lg text-justify whitespace-pre-line">
{{ selectedPost.abstractText }}
</p>
</div>
<div
class="flex flex-col md:flex-row justify-between items-start md:items-center gap-6 mb-8 pb-8 border-b dark:border-gray-800">
<div v-if="selectedPost.tags && selectedPost.tags.length > 0">
<h4 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Tags</h4>
<div class="flex flex-wrap gap-2">
<span v-for="tag in selectedPost.tags" :key="tag.id"
class="px-3 py-1 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 text-sm font-medium border border-gray-200 dark:border-gray-700">
#{{ tag.name }}
</span>
</div>
</div>
<div v-else class="text-sm text-gray-400 italic">No tags assigned</div>
<div class="flex items-center gap-4">
<div
class="flex items-center bg-slate-100 dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-1.5 shadow-sm">
<button class="p-2 rounded-lg transition-all active:scale-90"
:class="selectedPost.my_vote === 'UP' ? 'text-orange-600 bg-orange-100 dark:bg-orange-900/30' : 'text-slate-400 hover:text-orange-500 hover:bg-white dark:hover:bg-slate-700'"
title="Upvote" @click="votePost(selectedPost, 1)">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7" />
</svg>
</button>
<div class="min-w-[40px] text-center">
<span class="text-2xl font-black tracking-tight" :class="{
'text-orange-600': selectedPost.my_vote === 'UP',
'text-blue-600': selectedPost.my_vote === 'DOWN',
'text-slate-900 dark:text-white': !selectedPost.my_vote || selectedPost.my_vote === 'NONE'
}">
{{ selectedPost.rating }}
</span>
</div>
<button class="p-2 rounded-lg transition-all active:scale-90"
:class="selectedPost.my_vote === 'DOWN' ? 'text-blue-600 bg-blue-100 dark:bg-blue-900/30' : 'text-slate-400 hover:text-blue-500 hover:bg-white dark:hover:bg-slate-700'"
title="Downvote" @click="votePost(selectedPost, -1)">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
</div>
</div>
<div v-if="selectedPost.attachedFile"
class="mb-10 bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700 rounded-xl p-5 hover:border-blue-400 transition-colors group">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-red-100 text-red-600 rounded-lg group-hover:scale-110 transition-transform">
<svg class="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<div>
<div class="font-bold text-gray-900 dark:text-white mb-0.5">Attachment
Available</div>
<div class="text-xs text-gray-500 font-mono truncate max-w-[200px] md:max-w-md">
{{ getFileName(selectedPost.attachedFile) }}
</div>
</div>
</div>
<button
class="flex items-center gap-2 px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-lg shadow-lg shadow-blue-500/30 transform hover:-translate-y-0.5 transition-all"
@click="downloadFile(selectedPost)">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download
</button>
</div>
</div>
<hr class="border-gray-100 dark:border-gray-800 mb-8">
<section>
<h3 class="text-xl font-bold mb-6 flex items-center gap-2">
Comments
<span
class="bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 text-xs px-2 py-1 rounded-full">{{
selectedPost.comments.length }}</span>
</h3>
<div class="space-y-3">
<textarea v-model="newCommentContent" rows="3"
class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm"
placeholder="What are your thoughts?" />
<div class="flex justify-end">
<button :disabled="!newCommentContent.trim()"
class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-bold px-6 py-2 rounded-lg shadow transition-all active:scale-95"
@click="submitComment">
COMMENT
</button>
</div>
</div>
<div class="mt-8 space-y-6">
<div v-if="selectedPost.comments.length === 0" class="text-center py-8 text-gray-400 italic">
Be the first to comment!
</div>
<div v-for="comment in selectedPost.comments" :key="comment.id" class="flex gap-4">
<div
class="w-8 h-8 rounded-full bg-gradient-to-br from-gray-200 to-gray-300 dark:from-gray-700 dark:to-gray-600 flex-shrink-0" />
<div class="flex-1">
<div
class="bg-gray-50 dark:bg-gray-800/50 p-4 rounded-xl rounded-tl-none border border-gray-100 dark:border-gray-800">
<div class="flex items-center justify-between mb-2">
<span class="font-bold text-sm text-blue-600 dark:text-blue-400">{{
comment.author.name }}</span>
<span class="text-[10px] text-gray-400 uppercase">{{ new
Date(comment.created_at).toLocaleDateString() }}</span>
</div>
<p class="text-gray-700 dark:text-gray-300 text-sm leading-relaxed">
{{ comment.content }}
</p>
</div>
</div>
</div>
</div>
</section>
</div>
</article>
</div>
<CreatePostModal :is-open="isModalOpen" :post-to-edit="postToEdit" :api="api" :token="token"
@close="closeModal()" @refresh="handleRefresh" />
</main>
@@ -466,8 +261,6 @@ const editTagName = ref('');
const postToEdit = ref(null);
const newCommentContent = ref('');
const checkQueryForModal = () => {
if (route.query.openCreatePostModal === 'true') {
isModalOpen.value = true;
@@ -629,20 +422,6 @@ const handleRefresh = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
};
const submitComment = () => {
if (!newCommentContent.value.trim()) return;
const comment = {
id: Date.now(),
content: newCommentContent.value,
author: { name: user.value?.name || "Guest" },
created_at: new Date().toISOString()
};
selectedPost.value.comments.unshift(comment);
newCommentContent.value = '';
};
const loadPosts = async () => {
if (loading.value || !hasMore.value) return;
loading.value = true;
@@ -720,31 +499,6 @@ const votePost = async (targetPost, direction) => {
}
};
const getFileName = (fullPath) => {
if (!fullPath) return null;
return fullPath.split(/[/\\]/).pop();
};
const downloadFile = async (post) => {
if (!post.attachedFile) return;
const filename = getFileName(post.attachedFile);
const downloadUrl = `${api}/publications/download/${filename}`;
try {
const link = document.createElement('a');
link.href = downloadUrl;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (e) {
console.error("Download error:", e);
notifications.error("Could not download file.");
}
};
const filteredPosts = computed(() => {
const result = posts.value.filter(p => {
const matchesType = filterType.value === 'ALL' || p.type === filterType.value;