LET'S FUCKING GOOOOO #18

Merged
FernandoJVideira merged 33 commits from develop into master 2026-01-23 21:47:54 +00:00
5 changed files with 909 additions and 892 deletions
Showing only changes of commit e423497fd2 - Show all commits
+54 -25
View File
@@ -5,12 +5,14 @@ const props = defineProps<{
isOpen: boolean; isOpen: boolean;
api: string; api: string;
token: string | null; token: string | null;
postToEdit?: any;
}>(); }>();
const emit = defineEmits(['close', 'refresh']); const emit = defineEmits(['close', 'refresh']);
interface Tag { id: number; name: string; } interface Tag { id: number; name: string; }
const notifications = useNotificationStore();
const isSubmitting = ref(false); const isSubmitting = ref(false);
const availableTags = ref<Tag[]>([]); const availableTags = ref<Tag[]>([]);
const errors = reactive({ const errors = reactive({
@@ -73,26 +75,41 @@ async function submitPublication() {
authors: uploadForm.authors, authors: uploadForm.authors,
type: uploadForm.type, type: uploadForm.type,
abstract: uploadForm.abstract, abstract: uploadForm.abstract,
tags: uploadForm.selectedTagIds, tags: uploadForm.selectedTagIds
// Sending fixed defaults for removed fields to maintain API compatibility if needed
publication_date: new Date().toISOString().split('T')[0],
is_confidential: false
}; };
const formData = new FormData(); const isEditing = !!props.postToEdit;
if (uploadForm.file) formData.append('file', uploadForm.file); const url = isEditing
formData.append('data', JSON.stringify(metadata)); ? `${props.api}/publications/${props.postToEdit.id}`
: `${props.api}/publications`;
await $fetch(`${props.api}/publications`, { let requestBody: any;
method: 'POST', let headers: Record<string, string> = {
headers: { Authorization: `Bearer ${props.token}` }, Authorization: `Bearer ${props.token}`
body: formData };
if (!isEditing || uploadForm.file) {
const formData = new FormData();
if (uploadForm.file) formData.append('file', uploadForm.file);
formData.append('data', JSON.stringify(metadata));
requestBody = formData;
}
else {
requestBody = metadata;
}
await $fetch(url, {
method: isEditing ? 'PATCH' : 'POST',
headers,
body: requestBody
}); });
notifications.success(`Publication ${isEditing ? "updated" : "created"}`);
emit('refresh'); emit('refresh');
closeModal(); closeModal();
} catch (error) { } catch (error) {
alert("Upload failed."); console.error("Submission error:", error);
notifications.error("Operation failed");
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
} }
@@ -113,19 +130,34 @@ function handleFileChange(event: Event) {
} }
function closeModal() { function closeModal() {
uploadForm.title = '';
uploadForm.abstract = '';
uploadForm.type = 'CONFERENCE_PAPER';
uploadForm.newAuthorName = '';
uploadForm.authors = [];
uploadForm.selectedTagIds = [];
uploadForm.file = null;
emit('close'); emit('close');
} }
watch(() => props.isOpen, (newVal) => { watch(() => props.isOpen, (newVal) => {
if (newVal && availableTags.value.length === 0) fetchTags(); if (newVal && availableTags.value.length === 0) fetchTags();
if (!newVal) { if (!newVal) {
// Reset errors on close
errors.title = ''; errors.title = '';
errors.abstract = ''; errors.abstract = '';
} }
}); });
// Helper for colorful tags watch(() => props.postToEdit, (newPost) => {
if (newPost) {
uploadForm.title = newPost.title;
uploadForm.abstract = newPost.abstractText || newPost.abstract;
uploadForm.type = newPost.type;
uploadForm.authors = [...newPost?.authors];
uploadForm.selectedTagIds = newPost?.tags.map((t: any) => t.id);
}
}, { immediate: true });
const getTagClass = (id: number) => { const getTagClass = (id: number) => {
const colors = [ const colors = [
'bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-300', 'bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-300',
@@ -204,9 +236,6 @@ const getTagClass = (id: number) => {
<select v-model="uploadForm.type" <select v-model="uploadForm.type"
class="w-full rounded-lg border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm focus:ring-blue-500 sm:text-sm py-2.5 px-3 border"> class="w-full rounded-lg border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm focus:ring-blue-500 sm:text-sm py-2.5 px-3 border">
<option value="CONFERENCE_PAPER">Conference Paper</option> <option value="CONFERENCE_PAPER">Conference Paper</option>
<option value="ARTICLE">Article</option>
<option value="THESIS">Thesis</option>
<option value="REPORT">Report</option>
</select> </select>
</div> </div>
</div> </div>
@@ -224,7 +253,7 @@ const getTagClass = (id: number) => {
<div class="flex flex-col md:flex-row gap-8"> <div class="flex flex-col md:flex-row gap-8">
<div class="flex-1"> <div class="w-full">
<label <label
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Authors</label> class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Authors</label>
<div class="relative flex items-center"> <div class="relative flex items-center">
@@ -241,7 +270,7 @@ const getTagClass = (id: number) => {
</svg> </svg>
</button> </button>
</div> </div>
<div class="flex flex-wrap gap-2 mt-3"> <div :class="['flex flex-wrap gap-2', uploadForm.authors.length != 0 ? 'mt-3' : '']">
<span v-for="(author, index) in uploadForm.authors" :key="index" <span v-for="(author, index) in uploadForm.authors" :key="index"
class="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 text-xs font-medium border border-slate-200 dark:border-slate-700"> class="inline-flex items-center gap-1 px-2.5 py-1 rounded-md bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 text-xs font-medium border border-slate-200 dark:border-slate-700">
{{ author }} {{ author }}
@@ -255,7 +284,7 @@ const getTagClass = (id: number) => {
</div> </div>
</div> </div>
<div class="w-full md:w-auto min-w-[200px]"> <div class="flex-1 md:w-auto min-w-[300px]">
<label <label
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Tags</label> class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Tags</label>
<div <div
@@ -269,10 +298,10 @@ const getTagClass = (id: number) => {
if (idx > -1) uploadForm.selectedTagIds.splice(idx, 1); if (idx > -1) uploadForm.selectedTagIds.splice(idx, 1);
else uploadForm.selectedTagIds.push(tag.id); else uploadForm.selectedTagIds.push(tag.id);
}" :class="[ }" :class="[
uploadForm.selectedTagIds.includes(tag.id) uploadForm.selectedTagIds.includes(tag.id)
? getTagClass(tag.id) + ' border-transparent' ? getTagClass(tag.id) + ' border-transparent'
: 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400' : 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400'
]" ]"
class="px-3 py-1.5 rounded-full border text-xs font-bold uppercase tracking-tight transition-all shadow-sm whitespace-nowrap"> class="px-3 py-1.5 rounded-full border text-xs font-bold uppercase tracking-tight transition-all shadow-sm whitespace-nowrap">
{{ tag.name }} {{ tag.name }}
</button> </button>
@@ -286,7 +315,7 @@ const getTagClass = (id: number) => {
<button :disabled="isSubmitting" <button :disabled="isSubmitting"
class="inline-flex w-full justify-center rounded-lg bg-blue-600 px-6 py-2.5 text-sm font-bold text-white shadow-sm hover:bg-blue-500 disabled:opacity-50 sm:w-auto transition-all" class="inline-flex w-full justify-center rounded-lg bg-blue-600 px-6 py-2.5 text-sm font-bold text-white shadow-sm hover:bg-blue-500 disabled:opacity-50 sm:w-auto transition-all"
@click="submitPublication"> @click="submitPublication">
{{ isSubmitting ? 'Uploading...' : 'Submit Publication' }} {{ isSubmitting ? 'Uploading...' : 'Submit' }}
</button> </button>
<button <button
class="inline-flex w-full justify-center rounded-lg bg-white dark:bg-slate-800 px-6 py-2.5 text-sm font-bold text-slate-700 dark:text-slate-300 shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-700 hover:bg-slate-50 sm:w-auto transition-all" class="inline-flex w-full justify-center rounded-lg bg-white dark:bg-slate-800 px-6 py-2.5 text-sm font-bold text-slate-700 dark:text-slate-300 shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-700 hover:bg-slate-50 sm:w-auto transition-all"
+19 -22
View File
@@ -488,7 +488,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup>
import { ref, reactive, computed, onMounted } from 'vue'; import { ref, reactive, computed, onMounted } from 'vue';
import { useAuthStore } from "~/stores/auth-store.js"; import { useAuthStore } from "~/stores/auth-store.js";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
@@ -497,19 +497,18 @@ const config = useRuntimeConfig();
const api = config.public.apiBase; const api = config.public.apiBase;
const authStore = useAuthStore(); const authStore = useAuthStore();
const { token } = storeToRefs(authStore); const { token } = storeToRefs(authStore);
const notifications = useNotifications();
// State const users = ref([]);
const users = ref<any[]>([]);
const loading = ref(false); const loading = ref(false);
const showModal = ref(false); const showModal = ref(false);
const showDetailsModal = ref(false); const showDetailsModal = ref(false);
const isEditing = ref(false); const isEditing = ref(false);
const searchQuery = ref(''); const searchQuery = ref('');
const selectedUser = ref<any>(null); const selectedUser = ref(null);
const userHistory = ref<any[]>([]); const userHistory = ref([]);
// Mock history data generator const generateMockHistory = () => {
const generateMockHistory = (userId: number) => {
const actions = ['UPLOADED_PUBLICATION', 'ADDED_COMMENT', 'CORRECTED_PUBLICATION', 'RATED_PUBLICATION', 'TAGGED_PUBLICATION']; const actions = ['UPLOADED_PUBLICATION', 'ADDED_COMMENT', 'CORRECTED_PUBLICATION', 'RATED_PUBLICATION', 'TAGGED_PUBLICATION'];
const targetTypes = ['Publication', 'Comment', 'Review']; const targetTypes = ['Publication', 'Comment', 'Review'];
const details = [ const details = [
@@ -554,7 +553,6 @@ const generateMockHistory = (userId: number) => {
return history.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); return history.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
}; };
// Form Data
const formData = reactive({ const formData = reactive({
id: 0, id: 0,
name: '', name: '',
@@ -564,7 +562,6 @@ const formData = reactive({
is_active: true is_active: true
}); });
// Computed Stats & Filtering
const filteredUsers = computed(() => { const filteredUsers = computed(() => {
if (!searchQuery.value) return users.value; if (!searchQuery.value) return users.value;
const query = searchQuery.value.toLowerCase(); const query = searchQuery.value.toLowerCase();
@@ -577,11 +574,10 @@ const filteredUsers = computed(() => {
const activeCount = computed(() => users.value.filter(u => u.is_active).length); const activeCount = computed(() => users.value.filter(u => u.is_active).length);
const adminCount = computed(() => users.value.filter(u => u.role === 'ADMIN').length); const adminCount = computed(() => users.value.filter(u => u.role === 'ADMIN').length);
// --- API ACTIONS ---
const fetchUsers = async () => { const fetchUsers = async () => {
loading.value = true; loading.value = true;
try { try {
const data = await $fetch<any[]>(`${api}/admin/users`, { const data = await $fetch(`${api}/admin/users`, {
method: 'GET', method: 'GET',
headers: { Authorization: `Bearer ${token.value}` } headers: { Authorization: `Bearer ${token.value}` }
}); });
@@ -605,10 +601,11 @@ const createUser = async () => {
role: formData.role role: formData.role
} }
}); });
notifications.success("User created successfully");
closeModal(); closeModal();
fetchUsers(); fetchUsers();
} catch (error) { } catch (error) {
alert("Failed to create user."); notifications.error("Failed to create user");
} }
}; };
@@ -622,10 +619,11 @@ const updateUser = async () => {
is_active: formData.is_active is_active: formData.is_active
} }
}); });
notifications.success("User updated successfully");
closeModal(); closeModal();
fetchUsers(); fetchUsers();
} catch (error) { } catch (error) {
alert("Failed to update user."); notifications.error("Failed to update user");
} }
}; };
@@ -635,33 +633,32 @@ const sendPasswordRecovery = async () => {
try { try {
await $fetch(`${api}/auth/recover-password`, { await $fetch(`${api}/auth/recover-password`, {
method: 'POST', method: 'POST',
// headers: { Authorization: ... } // EP02 is usually public, but check your API requirements
body: { body: {
email: formData.email email: formData.email
} }
}); });
alert(`Recovery sent to ${formData.email}`); notifications.success(`Recovery sent to ${formData.email}`);
} catch (error) { } catch (error) {
alert("Failed to send recovery.");
console.error(error); console.error(error);
notifications.error("Failed to send recovery");
} }
}; };
const confirmDelete = async (user: any) => { const confirmDelete = async (user) => {
if (!confirm(`Are you sure you want to delete ${user.name}?`)) return; if (!confirm(`Are you sure you want to delete ${user.name}?`)) return;
try { try {
await $fetch(`${api}/admin/users/${user.id}`, { await $fetch(`${api}/admin/users/${user.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { Authorization: `Bearer ${token.value}` } headers: { Authorization: `Bearer ${token.value}` }
}); });
notifications.success("Deleted user");
fetchUsers(); fetchUsers();
} catch (error) { } catch (error) {
alert("Error deleting user."); notifications.error("Failed to delete user");
} }
}; };
// --- HELPERS --- const openUserDetails = (user) => {
const openUserDetails = (user: any) => {
selectedUser.value = user; selectedUser.value = user;
userHistory.value = generateMockHistory(user.id); userHistory.value = generateMockHistory(user.id);
showDetailsModal.value = true; showDetailsModal.value = true;
@@ -679,7 +676,7 @@ const openCreateModal = () => {
showModal.value = true; showModal.value = true;
}; };
const openEditModal = (user: any) => { const openEditModal = (user) => {
isEditing.value = true; isEditing.value = true;
formData.id = user.id; formData.id = user.id;
formData.name = user.name; formData.name = user.name;
@@ -708,7 +705,7 @@ const resetForm = () => {
formData.is_active = true; formData.is_active = true;
}; };
const getInitials = (name: string) => { const getInitials = (name) => {
if (!name) return '??'; if (!name) return '??';
return name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase(); return name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
}; };
+712 -597
View File
File diff suppressed because it is too large Load Diff
+123 -247
View File
@@ -2,6 +2,9 @@
<div <div
class="min-h-screen bg-slate-50 dark:bg-slate-950 font-sans text-slate-900 dark:text-slate-200 selection:bg-blue-100 dark:selection:bg-blue-900 pb-12 transition-colors duration-300"> class="min-h-screen bg-slate-50 dark:bg-slate-950 font-sans text-slate-900 dark:text-slate-200 selection:bg-blue-100 dark:selection:bg-blue-900 pb-12 transition-colors duration-300">
<CreatePostModal :is-open="isModalOpen" :post-to-edit="postToEdit" :api="api" :token="token" @close="closeModal"
@refresh="fetchProfileData" />
<Transition enter-active-class="transition duration-200 ease-out" enter-from-class="opacity-0" <Transition enter-active-class="transition duration-200 ease-out" enter-from-class="opacity-0"
enter-to-class="opacity-100" leave-active-class="transition duration-150 ease-in" enter-to-class="opacity-100" leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100" leave-to-class="opacity-0"> leave-from-class="opacity-100" leave-to-class="opacity-0">
@@ -20,7 +23,6 @@
class="w-full max-w-4xl bg-white dark:bg-gray-900 rounded-2xl shadow-2xl border dark:border-gray-800" class="w-full max-w-4xl bg-white dark:bg-gray-900 rounded-2xl shadow-2xl border dark:border-gray-800"
@click.stop> @click.stop>
<!-- Close Button Header -->
<div <div
class="sticky top-0 z-10 flex justify-between items-center p-4 bg-white/95 dark:bg-gray-900/95 backdrop-blur-sm border-b dark:border-gray-800 rounded-t-2xl"> class="sticky top-0 z-10 flex justify-between items-center p-4 bg-white/95 dark:bg-gray-900/95 backdrop-blur-sm border-b dark:border-gray-800 rounded-t-2xl">
<button <button
@@ -29,8 +31,9 @@
BACK TO FEED BACK TO FEED
</button> </button>
<button @click="selectedPost = null" <button
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"> class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
@click="selectedPost = null">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" /> d="M6 18L18 6M6 6l12 12" />
@@ -38,7 +41,6 @@
</button> </button>
</div> </div>
<!-- Post Content - Same as main page -->
<article class="overflow-hidden"> <article class="overflow-hidden">
<div <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"> 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">
@@ -136,10 +138,9 @@
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div <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"> 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 @click.stop="votePost(selectedPost, 1)" <button class="p-2 rounded-lg transition-all active:scale-90"
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'" :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"> title="Upvote" @click.stop="votePost(selectedPost, 1)">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2.5"> stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
@@ -155,10 +156,9 @@
{{ selectedPost.rating }} {{ selectedPost.rating }}
</span> </span>
</div> </div>
<button @click.stop="votePost(selectedPost, -1)" <button class="p-2 rounded-lg transition-all active:scale-90"
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'" :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"> title="Downvote" @click.stop="votePost(selectedPost, -1)">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" <svg class="w-6 h-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2.5"> stroke="currentColor" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
@@ -192,8 +192,9 @@
</div> </div>
</div> </div>
</div> </div>
<button @click.stop="downloadFile(selectedPost)" <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"> 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.stop="downloadFile(selectedPost)">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
@@ -318,14 +319,6 @@
</svg> </svg>
<span>Polytechnic of Leiria</span> <span>Polytechnic of Leiria</span>
</div> </div>
<div class="flex items-center gap-3 text-sm text-slate-600 dark:text-slate-400">
<svg class="w-4 h-4 text-slate-400 dark:text-slate-500 shrink-0" 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>
<span>Joined {{ formattedDate }}</span>
</div>
</div> </div>
</div> </div>
@@ -345,7 +338,7 @@
}}</div> }}</div>
<div <div
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-1"> class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-1">
Papers Publications
</div> </div>
</div> </div>
</div> </div>
@@ -387,46 +380,12 @@
Recent Publications Recent Publications
<span <span
class="bg-slate-200 dark:bg-slate-800 text-slate-700 dark:text-slate-300 text-xs font-bold px-2 py-0.5 rounded-full"> class="bg-slate-200 dark:bg-slate-800 text-slate-700 dark:text-slate-300 text-xs font-bold px-2 py-0.5 rounded-full">
{{ userProfile.publications.length }} Papers {{ userProfile.publications.length }} Publications
</span> </span>
</h2> </h2>
</div> </div>
<div class="flex flex-col gap-4"> <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 && !loading" <div v-if="userProfile.publications.length === 0 && !loading"
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"> 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">
@@ -449,21 +408,25 @@
<button <button
class="inline-flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-xl shadow-lg shadow-blue-500/20 hover:shadow-blue-500/30 transform active:scale-95 transition-all duration-200 cursor-pointer" class="inline-flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-xl shadow-lg shadow-blue-500/20 hover:shadow-blue-500/30 transform active:scale-95 transition-all duration-200 cursor-pointer"
@click="isModalOpen = true"> @click="goToCreatePost()">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <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" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" /> d="M12 4v16m8-8H4" />
</svg> </svg>
Submit your first post Submit your first publication
</button> </button>
</div> </div>
</div> </div>
<div v-for="post in userProfile.publications" :key="post.id" <div v-for="post in userProfile.publications" :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" :class="['relative 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 overflow-hidden', post.isHidden ? 'opacity-60 bg-gray-50' : 'bg-white']"
@click="fetchPostDetail(post.id)"> @click="fetchPostDetail(post.id)">
<div v-if="post.attachedFile"
class="absolute right-1 bottom-5 opacity-50 group-hover:opacity-100 transition-opacity duration-300">
<img src="/f.png" alt="attachment"
class="w-32 h-32 transform rotate-[-45deg] pointer-events-none select-none">
</div>
<div class="flex gap-4"> <div class="flex gap-4">
<div <div
@@ -482,7 +445,38 @@
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"> 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('_', ' ') }} {{ post.type.replace('_', ' ') }}
</span> </span>
<span class="text-xs text-gray-500">{{ post.publishedDate }}</span> <span class="text-xs text-gray-500">{{ new
Date(post.publicationDate).toLocaleDateString('en-US', {
year:
'numeric', month: 'long', day: 'numeric'
}) }}</span>
<button v-if="authStore.user.id == post.submitter.id"
class="p-1 text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-all transform hover:scale-110 active:scale-95 cursor-pointer"
title="Edit" @click.stop="openEditModal(post)">
<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="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.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.isHidden ? 'Unhide' : 'Hide'"
@click.stop="toggleHide(post)">
<svg width="16px" height="16px" viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path
d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z" />
<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.isHidden" x1="3" y1="3" x2="13" y2="13"
stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" />
</svg>
</button>
</div> </div>
<h2 class="text-lg font-bold text-gray-800 dark:text-white mb-2">{{ <h2 class="text-lg font-bold text-gray-800 dark:text-white mb-2">{{
post.title }}</h2> post.title }}</h2>
@@ -490,65 +484,16 @@
post.authors.join(', ') }}</p> post.authors.join(', ') }}</p>
<div <div
class="flex items-center gap-4 text-xs font-bold text-gray-500 uppercase"> class="flex items-center gap-4 text-xs font-bold text-gray-500 uppercase">
<span>💬 {{ post.comment_count }} Comments</span> <span>💬 {{ post.commentCount }} Comments</span>
<div class="flex gap-2"> <div class="flex gap-2">
<span v-for="tag in post.tags" :key="tag.id" <span v-for="tag in post.tags" :key="tag.id"
class="text-blue-600 dark:text-blue-400">#{{ tag.name }}</span> class="text-blue-600 dark:text-blue-400">#{{ tag.name }}</span>
</div> </div>
</div> </div>
</div> </div>
<div
class="flex items-center justify-end gap-2 group-hover/row:opacity-100 transition-opacity duration-200">
<button
class="p-2.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-all transform hover:scale-110 active:scale-95"
title="Edit" @click.stop="openEditModal(user)">
<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="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
class="p-2.5 text-slate-400 hover:text-amber-600 hover:bg-white-50 dark:hover:bg-white-900/20 rounded-lg transition-all transform hover:scale-110 active:scale-95"
title="Hide" @click.stop="confirmHide(post)">
<svg width="16px" height="16px" viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg" fill="currentColor"
class="bi bi-eye">
<path
d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z" />
<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" />
</svg>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<td class="p-6 pr-8 text-right">
<div
class="flex items-center justify-end gap-2 opacity-0 group-hover/row:opacity-100 transition-opacity duration-200">
<button
class="p-2.5 text-slate-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-all transform hover:scale-110 active:scale-95"
title="Edit" @click.stop="openEditModal(user)">
<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="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
class="p-2.5 text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-all transform hover:scale-110 active:scale-95"
title="Delete" @click.stop="confirmHide(user)">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</div> </div>
<div v-else-if="activeTab === 'activity'" key="activity" class="space-y-4"> <div v-else-if="activeTab === 'activity'" key="activity" class="space-y-4">
@@ -561,7 +506,7 @@
<div <div
class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 overflow-hidden"> class="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 overflow-hidden">
<div v-for="(item, index) in activityLog" :key="item.id" <div v-for="item in activityLog" :key="item.id"
class="p-4 flex gap-4 border-b border-slate-100 dark:border-slate-800 last:border-0 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors"> class="p-4 flex gap-4 border-b border-slate-100 dark:border-slate-800 last:border-0 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
<div class="flex flex-col items-center min-w-[80px] text-center pt-1"> <div class="flex flex-col items-center min-w-[80px] text-center pt-1">
@@ -604,7 +549,6 @@
<div v-else-if="activeTab === 'settings'" key="settings" <div v-else-if="activeTab === 'settings'" key="settings"
class="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 transition-colors"> class="bg-white dark:bg-slate-900 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 transition-colors">
<!-- Header -->
<div class="px-6 py-5 border-b border-slate-200 dark:border-slate-800"> <div class="px-6 py-5 border-b border-slate-200 dark:border-slate-800">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white"> <h2 class="text-lg font-semibold text-slate-900 dark:text-white">
Edit Profile Edit Profile
@@ -620,13 +564,13 @@
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"> class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Name Name
</label> </label>
<input id="name" type="text" v-model="newUsername" <input id="name" v-model="newUsername" type="text"
placeholder="Your first name and last name" class="w-full px-4 py-2.5 bg-white dark:bg-slate-800 placeholder="Your first name and last name" class="w-full px-4 py-2.5 bg-white dark:bg-slate-800
border border-slate-300 dark:border-slate-700 border border-slate-300 dark:border-slate-700
rounded-xl text-slate-900 dark:text-white rounded-xl text-slate-900 dark:text-white
placeholder-slate-400 placeholder-slate-400
focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:border-blue-500
outline-none transition-all" /> outline-none transition-all">
</div> </div>
<div> <div>
@@ -634,33 +578,34 @@
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"> class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
Email address Email address
</label> </label>
<input id="email" type="email" v-model="newEmail" placeholder="[email protected]" <input id="email" v-model="newEmail" type="email" placeholder="[email protected]"
class="w-full px-4 py-2.5 bg-white dark:bg-slate-800 class="w-full px-4 py-2.5 bg-white dark:bg-slate-800
border border-slate-300 dark:border-slate-700 border border-slate-300 dark:border-slate-700
rounded-xl text-slate-900 dark:text-white rounded-xl text-slate-900 dark:text-white
placeholder-slate-400 placeholder-slate-400
focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:border-blue-500
outline-none transition-all" /> outline-none transition-all">
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400"> <p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
Dont worry, Your email is going to be sold to russian hackers. Dont worry, Your email is going to be sold to russian hackers.
</p> </p>
</div> </div>
<a @click="openResetPassword" class="text-sm text-red dark:text-red-600 hover:underline"> <a class="text-sm text-red dark:text-red-600 hover:underline"
@click="openResetPassword">
Reset Password Reset Password
</a> </a>
</div> </div>
<!-- Footer / Actions -->
<div class="px-6 py-4 bg-slate-50 dark:bg-slate-800/50 <div class="px-6 py-4 bg-slate-50 dark:bg-slate-800/50
border-t border-slate-200 dark:border-slate-800 border-t border-slate-200 dark:border-slate-800
flex justify-end"> flex justify-end">
<button @click="updateProfile" class="inline-flex items-center gap-2
<button class="inline-flex items-center gap-2
bg-blue-600 hover:bg-blue-700 bg-blue-600 hover:bg-blue-700
text-white font-semibold text-white font-semibold
px-6 py-2.5 rounded-xl px-6 py-2.5 rounded-xl
shadow-sm transition-all shadow-sm transition-all
active:scale-95"> active:scale-95" @click="updateProfile">
Save changes Save changes
</button> </button>
</div> </div>
@@ -672,69 +617,32 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useAuthStore } from "~/stores/auth-store.js"; import { useAuthStore } from "~/stores/auth-store.js";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import CreatePostModal from '~/components/ui/modal/CreatePostModal.vue';
// --- Config & Store ---
const config = useRuntimeConfig(); const config = useRuntimeConfig();
const api = config.public.apiBase; const api = config.public.apiBase;
const authStore = useAuthStore(); const authStore = useAuthStore();
const { token } = storeToRefs(authStore); const { token } = storeToRefs(authStore);
const router = useRouter(); const router = useRouter();
const notifications = useNotificationStore();
// --- Reactive Variables --- const isModalOpen = ref(false);
const availableTags = ref<Tag[]>([]); // For tag select viewBox const postToEdit = ref(null);
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 ---
const selectedPost = ref(null); const selectedPost = ref(null);
const newCommentContent = ref(''); const newCommentContent = ref('');
const newUsername = ref(''); const newUsername = ref('');
const newEmail = ref(''); const newEmail = ref('');
interface Tag {
id: number;
name: string;
}
// Matches response from EP12 (/api/v1/publications/my-submissions) [cite: 296]
interface ApiPublication {
id: number;
title: string;
authors: string[];
publication_date: string;
type: string;
rating: number; // Mapping this to citations for the UI
comment_count: number;
attached_file?: string;
tags: Tag[];
abstract?: string;
}
// Matches your UI requirements
interface UIPublication {
id: number;
title: string;
abstract: string;
publishedDate: string;
publishedYear: string;
citations: number;
field: string;
}
// --- State ---
const loading = ref(true); const loading = ref(true);
const activeTab = ref('overview'); // State to control visible tab const activeTab = ref('overview');
const activityLog = ref<ActivityItem[]>([]); // Store history data const activityLog = ref([]);
const userProfile = ref({ const userProfile = ref({
id: 0, id: 0,
@@ -743,22 +651,49 @@ const userProfile = ref({
role: '', role: '',
is_active: false, is_active: false,
avatarUrl: '', avatarUrl: '',
publications: [] as any[] publications: []
}); });
// --- API Actions ---
const getFileName = (fullPath: string) => { const toggleHide = async (post) => {
try {
const newState = !post.isHidden;
await $fetch(`${api}/publications/${post.id}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token.value}` },
body: { isHidden: newState }
});
post.isHidden = newState;
notifications.success(`Post ${newState ? 'hidden' : 'restored'}`);
} catch (err) {
notifications.error("Failed to update post visibility");
}
};
const openEditModal = (post) => {
postToEdit.value = post;
isModalOpen.value = true;
};
const closeModal = () => {
isModalOpen.value = false;
postToEdit.value = null;
};
const getFileName = (fullPath) => {
if (!fullPath) return null; if (!fullPath) return null;
return fullPath.split(/[/\\]/).pop(); return fullPath.split(/[/\\]/).pop();
}; };
const downloadFile = async (post: any) => { const downloadFile = async (post) => {
if (!post.attachedFile) return; if (!post.attachedFile) return;
const filename = getFileName(post.attachedFile); const filename = getFileName(post.attachedFile);
const downloadUrl = `${api}/publications/download/${filename}`; // Verifica se este endpoint existe no teu backend const downloadUrl = `${api}/publications/download/${filename}`;
try { try {
// Método simples para download (se o endpoint for público ou lidar com cookies)
const link = document.createElement('a'); const link = document.createElement('a');
link.href = downloadUrl; link.href = downloadUrl;
link.setAttribute('download', filename || 'download'); link.setAttribute('download', filename || 'download');
@@ -770,21 +705,7 @@ const downloadFile = async (post: any) => {
} }
}; };
// Fetch Tags (EP23) - To populate the select box const fetchPostDetail = async (id) => {
async function fetchTags() {
if (!token.value) return;
try {
const data = await $fetch<Tag[]>(`${api}/tags`, {
method: 'GET',
headers: { Authorization: `Bearer ${token.value}` }
});
availableTags.value = data || [];
} catch (e) {
console.error("Failed to fetch tags", e);
}
}
const fetchPostDetail = async (id: number) => {
loading.value = true; loading.value = true;
try { try {
const postDetail = await $fetch(`${api}/publications/${id}`, { const postDetail = await $fetch(`${api}/publications/${id}`, {
@@ -802,27 +723,15 @@ const fetchPostDetail = async (id: number) => {
} }
}; };
// --- Computed ---
const totalCitations = computed(() => const totalCitations = computed(() =>
userProfile.value.publications.reduce((sum, pub) => sum + (pub.citations || 0), 0) userProfile.value.publications.reduce((sum, pub) => sum + (pub.citations || 0), 0)
); );
const formattedDate = computed(() => {
return new Date(userProfile.value.joinDate).toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
});
});
// --- API Actions ---
// 1. Fetch Activity Log (EP06)
async function fetchActivityHistory() { async function fetchActivityHistory() {
if (!token.value) return; if (!token.value) return;
try { try {
// GET /api/v1/me/history const data = await $fetch(`${api}/me/history`, {
const data = await $fetch<ActivityItem[]>(`${api}/me/history`, {
method: 'GET', method: 'GET',
headers: { Authorization: `Bearer ${token.value}` } headers: { Authorization: `Bearer ${token.value}` }
}); });
@@ -832,7 +741,6 @@ async function fetchActivityHistory() {
} }
} }
// 2. Fetch Main Profile Data (EP03 & EP12)
async function fetchProfileData() { async function fetchProfileData() {
if (!token.value) return; if (!token.value) return;
loading.value = true; loading.value = true;
@@ -845,12 +753,6 @@ async function fetchProfileData() {
const publicationsData = await $fetch(`${api}/publications/me`, { const publicationsData = await $fetch(`${api}/publications/me`, {
method: 'GET', method: 'GET',
headers: { Authorization: `Bearer ${token.value}` }, headers: { Authorization: `Bearer ${token.value}` },
params: {
page: page.value,
sort: sortBy.value,
order: sortDesc.value ? 'desc' : 'asc',
type: filterType.value !== 'ALL' ? filterType.value : undefined
}
}); });
userProfile.value = { userProfile.value = {
@@ -863,16 +765,14 @@ async function fetchProfileData() {
avatarUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(userData.name)}&background=0D8ABC&color=fff` avatarUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(userData.name)}&background=0D8ABC&color=fff`
}; };
userProfile.value.publications = (publicationsData as any[]).map(pub => { userProfile.value.publications = (publicationsData).map(pub => {
const pubDate = new Date(pub.publication_date); const pubDate = new Date(pub.publicationDate);
return { return {
...pub, // Manter todos os campos originais da API ...pub,
publishedDate: pubDate.toLocaleDateString(), publishedDate: pubDate.toLocaleDateString(),
publishedYear: pubDate.getFullYear().toString(), publishedYear: pubDate.getFullYear().toString(),
citations: pub.rating || 0, citations: pub.rating || 0,
authors: pub.authors || [], authors: pub.authors || [],
tags: pub.tags || [], tags: pub.tags || [],
comment_count: pub.comment_count || 0, comment_count: pub.comment_count || 0,
@@ -914,33 +814,18 @@ const votePost = async (targetPost, direction) => {
selectedPost.value.my_vote = response.my_vote; selectedPost.value.my_vote = response.my_vote;
} }
notifications.success("Voted successfully");
} catch (error) { } catch (error) {
console.error("Vote failed", error); console.error("Vote failed", error);
alert("Failed to submit vote."); notifications.error("Failed to voted");
} }
}; };
const confirmHide = async (post) => { const goToCreatePost = () => {
if (!post) return; router.push({
path: '/',
const confirmed = confirm(`Are you sure you want to hide the post titled "${post.title}"?`); query: { openCreatePostModal: 'true' }
});
if (!confirmed) return;
try {
await $fetch(`${api}/publications/${post.id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token.value}` }
});
userProfile.value.publications = userProfile.value.publications.filter(p => p.id !== post.id);
alert("Post deleted successfully.");
} catch (error) {
console.error("Delete failed", error);
alert("Failed to delete the post.");
}
}; };
const updateProfile = async () => { const updateProfile = async () => {
@@ -953,7 +838,7 @@ const updateProfile = async () => {
const email = newEmail.value.trim(); const email = newEmail.value.trim();
if (name.length === 0 || email.length === 0) { if (name.length === 0 || email.length === 0) {
alert("Name and email cannot be empty."); notifications.error("Name and email cannot be empty");
return; return;
} }
@@ -965,35 +850,26 @@ const updateProfile = async () => {
email: email email: email
} }
}); });
alert("Profile updated successfully. Please log in again."); notifications.success("Profile updated successfully");
authStore.logout(); authStore.logout();
} catch (error) { } catch (error) {
console.error("Profile update failed", error); console.error("Profile update failed", error);
alert("Failed to update profile."); notifications.error("Failed to update profile");
} }
fetchProfileData(); fetchProfileData();
}; };
// --- Redirects ---
const goToCreatePost = () => {
console.log("Navigating to create post modal...");
router.push({
path: '/',
query: { openCreatePostModal: 'true' }
});
};
const openResetPassword = () => { const openResetPassword = () => {
router.push({ path: '/profile/update-password' }); router.push({ path: '/profile/update-password' });
}; };
// --- Lifecycle ---
onMounted(() => { onMounted(() => {
fetchProfileData(); fetchProfileData();
fetchActivityHistory(); // Fetch history on mount fetchActivityHistory();
}); });
</script> </script>
<style scoped> <style scoped>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB