feat: admin panel

This commit is contained in:
AfonsoCMSousa
2026-01-22 15:34:33 +00:00
parent 77c05ee490
commit c2bb9a9b77
2 changed files with 719 additions and 380 deletions
+606 -355
View File
File diff suppressed because it is too large Load Diff
+112 -24
View File
@@ -140,6 +140,41 @@
</div>
<div class="flex flex-col gap-4">
<div
class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4">
<div class="flex flex-wrap items-center gap-2 w-full sm:w-auto">
<div class="relative flex-1 sm:flex-none">
<input v-model="searchQuery" type="text" placeholder="Search posts..."
class="w-full sm:w-48 bg-white dark:bg-gray-900 border dark:border-gray-800 rounded-lg pl-3 pr-3 py-2 text-xs font-bold outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex items-center">
<select v-model="sortBy"
class="bg-white dark:bg-gray-900 border dark:border-gray-800 rounded-l-lg px-3 py-2 text-xs font-bold uppercase outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer border-r-0">
<option value="date">Date</option>
<option value="votes">Rating</option>
<option value="comments">Comments</option>
</select>
<button
class="bg-white dark:bg-gray-900 border dark:border-gray-800 rounded-r-lg px-3 py-2 text-xs font-bold hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
:title="sortDesc ? 'Descending' : 'Ascending'"
@click="sortDesc = !sortDesc">
<span class="inline-block transition-transform duration-200"
:class="{ 'rotate-180': !sortDesc }">
</span>
</button>
</div>
<select v-model="filterType"
class="bg-white dark:bg-gray-900 border dark:border-gray-800 rounded-lg px-3 py-2 text-xs font-bold uppercase outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer">
<option value="ALL">All Types</option>
<option value="CONFERENCE_PAPER">Conference Paper</option>
<option value="RESEARCH_POST">Research Post</option>
</select>
</div>
</div>
<div v-if="userProfile.publications.length === 0"
class="relative overflow-hidden rounded-2xl border-2 border-dashed border-slate-300 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50 p-12 text-center hover:border-blue-400 dark:hover:border-blue-500 transition-all duration-300 group">
@@ -177,25 +212,50 @@
</div>
</div>
<div v-for="pub in userProfile.publications" :key="pub.id"
class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 hover:shadow-md hover:border-blue-300 dark:hover:border-blue-700 transition-all duration-200 group relative">
<div class="flex justify-between items-start gap-4 mb-2">
<h3 class="text-base font-bold text-slate-900 dark:text-slate-100">{{ pub.title
}}</h3>
<span
class="shrink-0 px-2.5 py-1 rounded-md bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-xs font-bold border border-slate-200 dark:border-slate-700">
{{ pub.publishedYear }}
</span>
</div>
<p class="text-slate-600 dark:text-slate-400 text-sm leading-relaxed line-clamp-2">
{{ pub.abstract }}</p>
<div
class="mt-4 pt-4 border-t border-slate-50 dark:border-slate-800 flex flex-wrap items-center gap-4 text-xs font-medium">
<span
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-100 dark:border-blue-800/50">
{{ pub.field }}
</span>
<span>{{ pub.citations }} Citations</span>
<div v-for="post in userProfile.publicationsData" :key="post.id"
class="group bg-white dark:bg-gray-900 border dark:border-gray-800 rounded-xl p-5 shadow-sm hover:border-blue-500/50 transition-all cursor-pointer mb-4"
@click="fetchPostDetail(post.id)">
<div class="flex gap-4">
<div
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"
: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"
:class="{ 'text-blue-500': post.my_vote === 'DOWN' }"
@click.stop="votePost(post, -1)"></button>
</div>
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400">
{{ post.type.replace('_', ' ') }}
</span>
<span class="text-xs text-gray-500">{{ post.publication_date
}}</span>
</div>
<h2 class="text-lg font-bold text-gray-800 dark:text-white mb-2">{{
post.title }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">By
{{
post.authors.join(', ') }}
</p>
<div
class="flex items-center gap-4 text-xs font-bold text-gray-500 uppercase">
<span>💬 {{ post.comment_count }} Comments</span>
<div class="flex gap-2">
<span v-for="tag in post.tags" :key="tag.id"
class="text-blue-600 dark:text-blue-400">#{{ tag.name
}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -282,6 +342,14 @@ const authStore = useAuthStore();
const { token } = storeToRefs(authStore);
const router = useRouter();
// --- Reactive Variables ---
const availableTags = ref<Tag[]>([]); // For tag select viewBox
const searchQuery = ref('');
const sortBy = ref('date'); // 'date' | 'votes' | 'comments'
const sortDesc = ref(true);
const filterType = ref('ALL'); // 'ALL' | 'CONFERENCE_PAPER' | 'RESEARCH_POST'
const page = ref(1);
// --- Interfaces ---
interface Tag {
@@ -348,6 +416,20 @@ async function fetchTags() {
}
}
const fetchPostDetail = async (id) => {
loading.value = true;
try {
const postDetail = await $fetch(`${api}/publications/${id}`, {
headers: { Authorization: `Bearer ${token.value}` }
});
selectedPost.value = postDetail;
} catch (err) {
console.error("Failed to fetch post detail:", err);
} finally {
loading.value = false;
}
};
// --- Computed ---
const totalCitations = computed(() =>
userProfile.value.publications.reduce((sum, pub) => sum + (pub.citations || 0), 0)
@@ -390,11 +472,17 @@ async function fetchProfileData() {
headers: { Authorization: `Bearer ${token.value}` }
});
// Fetch Publications
// const publicationsData = await $fetch(`${api}/publications/my-submissions`, {
// method: 'GET',
// headers: { Authorization: `Bearer ${token.value}` }
// });
// Fetch User Publications
const publicationsData = await $fetch(`${api}/publications/me`, {
method: 'GET',
headers: { Authorization: `Bearer ${token.value}` },
params: {
page: page.value,
sort: sortBy.value,
order: sortDesc.value ? 'desc' : 'asc',
type: filterType.value !== 'ALL' ? filterType.value : undefined
}
});
// Map Data
userProfile.value = {