Merge pull request 'home-page functionality, and some design tweaks' (#10) from feature/home-page into develop
Reviewed-on: https://gitea.fernandovideira.com/SyntaxSquad/DAEProject_Frontend/pulls/10 Reviewed-by: Afonso Clerigo Mendes de Sousa <[email protected]> Reviewed-by: FernandoJVideira <[email protected]>
This commit was merged in pull request #10.
This commit is contained in:
@@ -4,6 +4,7 @@ import Toast from './components/ui/toast/Toast.vue';
|
||||
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<Title>Reddit for scientists</Title>
|
||||
<Toast />
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
--text-tertiary: #6e7681;
|
||||
--text-link: #2f81f7;
|
||||
--text-link-hover: #539bf5;
|
||||
--text-slate: #57606a;
|
||||
|
||||
--border-primary: #30363d;
|
||||
--border-hover: #8d96a0;
|
||||
@@ -60,17 +59,14 @@ h1, h2, h3, h4, h5, h6 {
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-link);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--text-link-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -81,8 +77,7 @@ code {
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
SEMANTIC COMPONENTS (SAFE)
|
||||
============================================== */
|
||||
/* SEMANTIC COMPONENTS (SAFE) */
|
||||
|
||||
|
||||
.card {
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
api: string;
|
||||
token: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'refresh']);
|
||||
|
||||
interface Tag { id: number; name: string; }
|
||||
|
||||
const isSubmitting = ref(false);
|
||||
const availableTags = ref<Tag[]>([]);
|
||||
const errors = reactive({
|
||||
title: '',
|
||||
abstract: ''
|
||||
});
|
||||
|
||||
const uploadForm = reactive({
|
||||
title: '',
|
||||
abstract: '',
|
||||
type: 'CONFERENCE_PAPER',
|
||||
newAuthorName: '',
|
||||
authors: [] as string[],
|
||||
selectedTagIds: [] as number[],
|
||||
file: null as File | null
|
||||
});
|
||||
|
||||
// --- Validation Logic ---
|
||||
const validate = () => {
|
||||
let isValid = true;
|
||||
errors.title = '';
|
||||
errors.abstract = '';
|
||||
|
||||
if (!uploadForm.title.trim()) {
|
||||
errors.title = 'Title is required';
|
||||
isValid = false;
|
||||
} else if (uploadForm.title.length > 50) {
|
||||
errors.title = 'Title cannot exceed 50 characters';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!uploadForm.abstract.trim()) {
|
||||
errors.abstract = 'Abstract is required';
|
||||
isValid = false;
|
||||
} else if (uploadForm.abstract.length > 2000) {
|
||||
errors.abstract = 'Abstract cannot exceed 2000 characters';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
async function fetchTags() {
|
||||
if (!props.token) return;
|
||||
try {
|
||||
const data = await $fetch<Tag[]>(`${props.api}/tags`, {
|
||||
headers: { Authorization: `Bearer ${props.token}` }
|
||||
});
|
||||
availableTags.value = data || [];
|
||||
} catch (e) { console.error("Tags fetch failed", e); }
|
||||
}
|
||||
|
||||
async function submitPublication() {
|
||||
if (!validate()) return;
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
const metadata = {
|
||||
title: uploadForm.title,
|
||||
authors: uploadForm.authors,
|
||||
type: uploadForm.type,
|
||||
abstract: uploadForm.abstract,
|
||||
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();
|
||||
if (uploadForm.file) formData.append('file', uploadForm.file);
|
||||
formData.append('data', JSON.stringify(metadata));
|
||||
|
||||
await $fetch(`${props.api}/publications`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${props.token}` },
|
||||
body: formData
|
||||
});
|
||||
|
||||
emit('refresh');
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
alert("Upload failed.");
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function addAuthor() {
|
||||
if (uploadForm.newAuthorName.trim()) {
|
||||
uploadForm.authors.push(uploadForm.newAuthorName.trim());
|
||||
uploadForm.newAuthorName = '';
|
||||
}
|
||||
}
|
||||
|
||||
function removeAuthor(index: number) { uploadForm.authors.splice(index, 1); }
|
||||
|
||||
function handleFileChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files?.[0]) uploadForm.file = input.files[0];
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
emit('close');
|
||||
}
|
||||
|
||||
watch(() => props.isOpen, (newVal) => {
|
||||
if (newVal && availableTags.value.length === 0) fetchTags();
|
||||
if (!newVal) {
|
||||
// Reset errors on close
|
||||
errors.title = '';
|
||||
errors.abstract = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Helper for colorful tags
|
||||
const getTagClass = (id: number) => {
|
||||
const colors = [
|
||||
'bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-300',
|
||||
'bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
'bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-300',
|
||||
'bg-cyan-100 text-cyan-700 border-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-300',
|
||||
'bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300',
|
||||
'bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
];
|
||||
return colors[id % colors.length];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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" leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 overflow-y-auto">
|
||||
<div class="fixed inset-0 bg-slate-900/75 backdrop-blur-sm" @click="closeModal" />
|
||||
|
||||
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<div
|
||||
class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-900 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl border border-slate-200 dark:border-slate-800">
|
||||
|
||||
<div
|
||||
class="bg-slate-50 dark:bg-slate-800/50 px-6 py-4 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center">
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white">Submit New Publication</h3>
|
||||
<button class="text-slate-400 hover:text-slate-500 dark:hover:text-slate-300"
|
||||
@click="closeModal">
|
||||
<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"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-6 max-h-[80vh] overflow-y-auto space-y-6">
|
||||
<div
|
||||
class="flex justify-center rounded-lg border-2 border-dashed border-slate-300 dark:border-slate-700 px-6 py-8 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-slate-400" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<div
|
||||
class="mt-4 flex text-sm leading-6 text-slate-600 dark:text-slate-400 justify-center">
|
||||
<label for="file-upload"
|
||||
class="relative cursor-pointer rounded-md font-semibold text-blue-600 hover:text-blue-500">
|
||||
<span>Upload a file</span>
|
||||
<input id="file-upload" type="file" class="sr-only" accept=".pdf,.zip"
|
||||
@change="handleFileChange">
|
||||
</label>
|
||||
<p class="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
<p v-if="uploadForm.file"
|
||||
class="mt-2 text-sm font-bold text-green-600 dark:text-green-400 text-center">
|
||||
Selected: {{ uploadForm.file.name }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="md:col-span-2">
|
||||
<label
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Title</label>
|
||||
<input v-model="uploadForm.title" type="text"
|
||||
:class="[errors.title ? 'border-red-500' : 'border-slate-300 dark:border-slate-700']"
|
||||
class="w-full rounded-lg 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"
|
||||
placeholder="e.g. A New Technique for Data Science">
|
||||
<p v-if="errors.title" class="mt-1 text-xs text-red-500">{{ errors.title }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Type</label>
|
||||
<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">
|
||||
<option value="CONFERENCE_PAPER">Conference Paper</option>
|
||||
<option value="ARTICLE">Article</option>
|
||||
<option value="THESIS">Thesis</option>
|
||||
<option value="REPORT">Report</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Abstract ({{ uploadForm.abstract.length }}/2000)
|
||||
</label>
|
||||
<textarea v-model="uploadForm.abstract" rows="4"
|
||||
:class="[errors.abstract ? 'border-red-500' : 'border-slate-300 dark:border-slate-700']"
|
||||
class="w-full rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm focus:ring-blue-500 sm:text-sm py-2 px-3 border"
|
||||
placeholder="Enter a brief summary..." />
|
||||
<p v-if="errors.abstract" class="mt-1 text-xs text-red-500">{{ errors.abstract }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-8">
|
||||
|
||||
<div class="flex-1">
|
||||
<label
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Authors</label>
|
||||
<div class="relative flex items-center">
|
||||
<input v-model="uploadForm.newAuthorName" type="text"
|
||||
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 text-sm py-2 pl-3 pr-10 border"
|
||||
placeholder="Name" @keyup.enter="addAuthor">
|
||||
<button type="button"
|
||||
class="absolute right-2 p-1 text-blue-600 hover:text-blue-700 transition-colors"
|
||||
@click="addAuthor">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
stroke-width="3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mt-3">
|
||||
<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">
|
||||
{{ author }}
|
||||
<button @click="removeAuthor(index)">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-auto min-w-[200px]">
|
||||
<label
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Tags</label>
|
||||
<div
|
||||
class="overflow-y-auto border border-slate-200 dark:border-slate-700 rounded-lg p-3 bg-slate-50/50 dark:bg-slate-800/30 flex flex-wrap gap-2 content-start">
|
||||
<label v-if="availableTags.length == 0"
|
||||
class="relative rounded-md font-semibold text-blue-600">
|
||||
<span>No tags</span>
|
||||
</label>
|
||||
<button v-for="tag in availableTags" :key="tag.id" type="button" @click="() => {
|
||||
const idx = uploadForm.selectedTagIds.indexOf(tag.id);
|
||||
if (idx > -1) uploadForm.selectedTagIds.splice(idx, 1);
|
||||
else uploadForm.selectedTagIds.push(tag.id);
|
||||
}" :class="[
|
||||
uploadForm.selectedTagIds.includes(tag.id)
|
||||
? 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'
|
||||
]"
|
||||
class="px-3 py-1.5 rounded-full border text-xs font-bold uppercase tracking-tight transition-all shadow-sm whitespace-nowrap">
|
||||
{{ tag.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-slate-50 dark:bg-slate-800/50 px-6 py-4 flex flex-row-reverse gap-3 border-t border-slate-100 dark:border-slate-800">
|
||||
<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"
|
||||
@click="submitPublication">
|
||||
{{ isSubmitting ? 'Uploading...' : 'Submit Publication' }}
|
||||
</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"
|
||||
@click="closeModal">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
+35
-40
@@ -6,33 +6,25 @@
|
||||
|
||||
<nuxt-link to="/" class="flex items-center gap-3 group flex-shrink-0">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg blur opacity-25 group-hover:opacity-40 transition duration-300"></div>
|
||||
<div
|
||||
class="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-lg blur opacity-25 group-hover:opacity-40 transition duration-300" />
|
||||
<div
|
||||
class="relative w-10 h-10 rounded-lg bg-gradient-to-br from-blue-600 to-indigo-600 flex items-center justify-center text-white font-bold text-lg shadow-lg shadow-blue-500/30">
|
||||
A
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xl font-bold text-slate-900 dark:text-white tracking-tight hidden md:block">
|
||||
Academic<span class="bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent">Sys</span>
|
||||
Academic<span
|
||||
class="bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent">Sys</span>
|
||||
</span>
|
||||
</nuxt-link>
|
||||
|
||||
<div class="flex items-center gap-4 md:gap-8">
|
||||
|
||||
<div class="hidden md:flex items-center gap-1">
|
||||
<nuxt-link v-for="link in navLinks" :key="link.path" :to="link.path"
|
||||
class="px-4 py-2 rounded-lg text-sm font-semibold text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all duration-200"
|
||||
active-class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 text-blue-600 dark:text-blue-400 shadow-sm">
|
||||
{{ link.name }}
|
||||
</nuxt-link>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:block h-6 w-px bg-slate-200 dark:bg-slate-700 mx-2"></div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
<button @click="toggleDark"
|
||||
class="p-2.5 rounded-full text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-700 dark:hover:text-slate-300 transition-all focus:outline-none focus:ring-2 focus:ring-blue-500/50">
|
||||
<button
|
||||
class="p-2.5 rounded-full text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-700 dark:hover:text-slate-300 transition-all focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
@click="toggleDark">
|
||||
<svg v-if="!isDark" 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="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
@@ -43,25 +35,21 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div v-if="authStore?.token" class="relative" ref="userMenuRef">
|
||||
<button @click.stop="isUserMenuOpen = !isUserMenuOpen"
|
||||
<div v-if="authStore?.token" ref="userMenuRef" class="relative">
|
||||
<button
|
||||
class="flex items-center gap-3 pl-3 pr-2 py-1.5 rounded-full border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 hover:border-slate-300 dark:hover:border-slate-600 transition-all focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
||||
:class="{ 'bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600': isUserMenuOpen }">
|
||||
<span
|
||||
class="text-sm font-semibold text-slate-700 dark:text-slate-200 hidden sm:block">
|
||||
:class="{ 'bg-slate-50 dark:bg-slate-800 border-slate-300 dark:border-slate-600': isUserMenuOpen }"
|
||||
@click.stop="isUserMenuOpen = !isUserMenuOpen">
|
||||
<span class="text-sm font-semibold text-slate-700 dark:text-slate-200 hidden sm:block">
|
||||
{{ authStore.user.name.split(' ')[0] }}
|
||||
</span>
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-full blur opacity-30"></div>
|
||||
<div
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-full blur opacity-30" />
|
||||
<img :src="authStore.user.avatarUrl || `https://ui-avatars.com/api/?name=${authStore.user.name}&background=0D8ABC&color=fff`"
|
||||
alt="Avatar"
|
||||
class="relative w-9 h-9 rounded-full border-2 border-white dark:border-slate-800 object-cover shadow-md">
|
||||
</div>
|
||||
<svg class="w-4 h-4 text-slate-400 transition-transform duration-200" :class="{ 'rotate-180': isUserMenuOpen }"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<transition enter-active-class="transition ease-out duration-100"
|
||||
@@ -73,32 +61,40 @@
|
||||
<div v-if="isUserMenuOpen"
|
||||
class="absolute right-0 mt-2 w-64 bg-white dark:bg-slate-900 rounded-xl shadow-xl border border-slate-200 dark:border-slate-800 py-2 focus:outline-none z-50 ring-1 ring-black/5 dark:ring-white/5">
|
||||
<div class="px-4 py-3 border-b border-slate-100 dark:border-slate-800 mb-1">
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 font-semibold uppercase tracking-wide">Signed in as
|
||||
<p
|
||||
class="text-xs text-slate-500 dark:text-slate-400 font-semibold uppercase tracking-wide">
|
||||
Signed in as
|
||||
</p>
|
||||
<p class="text-sm font-bold text-slate-900 dark:text-white truncate mt-1">{{
|
||||
authStore.user.email }}</p>
|
||||
</div>
|
||||
<button @click="navigateTo('/profile')"
|
||||
class="w-full text-left px-4 py-2.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-3 transition-colors">
|
||||
<button
|
||||
class="w-full text-left px-4 py-2.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-3 transition-colors"
|
||||
@click="navigateTo('/profile')">
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
Your Profile
|
||||
</button>
|
||||
<div v-if="authStore.user.role == 'ADMINISTRATOR'">
|
||||
<button @click="navigateTo('/admin/users')"
|
||||
class="w-full text-left px-4 py-2.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-3 transition-colors">
|
||||
<button
|
||||
class="w-full text-left px-4 py-2.5 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-3 transition-colors"
|
||||
@click="navigateTo('/admin/users')">
|
||||
<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="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-10c1.657 0 3 .895 3 2s-1.343 2-3 2-3 .895-3 2 1.343 2 3 2m0-10v10m0 0l3-3m-3 3l-3-3" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-10c1.657 0 3 .895 3 2s-1.343 2-3 2-3 .895-3 2 1.343 2 3 2m0-10v10m0 0l3-3m-3 3l-3-3" />
|
||||
</svg>
|
||||
Admin Dashboard
|
||||
</button>
|
||||
</div>
|
||||
<div class="border-t border-slate-100 dark:border-slate-800 mt-1 pt-1">
|
||||
<button @click="handleLogout"
|
||||
class="w-full text-left px-4 py-2.5 text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-3 transition-colors">
|
||||
<button
|
||||
class="w-full text-left px-4 py-2.5 text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-3 transition-colors"
|
||||
@click="handleLogout">
|
||||
<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="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
Sign out
|
||||
</button>
|
||||
@@ -114,8 +110,9 @@
|
||||
</nuxt-link>
|
||||
</template>
|
||||
|
||||
<button @click="isMobileMenuOpen = !isMobileMenuOpen"
|
||||
class="md:hidden p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
|
||||
<button
|
||||
class="md:hidden p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
@click="isMobileMenuOpen = !isMobileMenuOpen">
|
||||
<svg v-if="!isMobileMenuOpen" class="w-7 h-7" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
@@ -188,7 +185,6 @@ function toggleDark() {
|
||||
|
||||
// Navigation Links
|
||||
const navLinks = [
|
||||
{ name: 'Home', path: '/' },
|
||||
{ name: 'Profile', path: '/profile' },
|
||||
];
|
||||
|
||||
@@ -226,4 +222,3 @@ onUnmounted(() => {
|
||||
window.removeEventListener('click', closeDropdowns);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<title>Team Management - Reddit for Academic Papers</title>
|
||||
<div
|
||||
class="min-h-screen bg-slate-50 dark:bg-slate-950 p-4 md:p-8 font-sans text-slate-900 dark:text-slate-200 transition-colors duration-300">
|
||||
|
||||
@@ -15,11 +14,11 @@
|
||||
Manage user accounts, roles, and system access permissions
|
||||
</p>
|
||||
</div>
|
||||
<button @click="openCreateModal"
|
||||
class="group relative inline-flex items-center gap-2.5 px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-xl shadow-lg shadow-blue-500/30 hover:shadow-blue-500/50 transform hover:-translate-y-0.5 active:translate-y-0 transition-all duration-200">
|
||||
<button
|
||||
class="group relative inline-flex items-center gap-2.5 px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold rounded-xl shadow-lg shadow-blue-500/30 hover:shadow-blue-500/50 transform hover:-translate-y-0.5 active:translate-y-0 transition-all duration-200"
|
||||
@click="openCreateModal">
|
||||
<div
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl blur opacity-30 group-hover:opacity-50 transition duration-200">
|
||||
</div>
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl blur opacity-30 group-hover:opacity-50 transition duration-200" />
|
||||
<svg class="relative w-5 h-5 transition-transform group-hover:rotate-90 duration-300" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
@@ -32,13 +31,12 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl blur opacity-0 group-hover:opacity-20 transition duration-300">
|
||||
</div>
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl blur opacity-0 group-hover:opacity-20 transition duration-300" />
|
||||
<div
|
||||
class="relative bg-white dark:bg-slate-900 p-6 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm hover:shadow-md transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-1 bg-blue-500/20 rounded-xl blur"></div>
|
||||
<div class="absolute -inset-1 bg-blue-500/20 rounded-xl blur" />
|
||||
<div
|
||||
class="relative p-3 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 text-white shadow-lg shadow-blue-500/30">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -62,13 +60,12 @@
|
||||
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-emerald-600 to-teal-600 rounded-2xl blur opacity-0 group-hover:opacity-20 transition duration-300">
|
||||
</div>
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-emerald-600 to-teal-600 rounded-2xl blur opacity-0 group-hover:opacity-20 transition duration-300" />
|
||||
<div
|
||||
class="relative bg-white dark:bg-slate-900 p-6 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm hover:shadow-md transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-1 bg-emerald-500/20 rounded-xl blur"></div>
|
||||
<div class="absolute -inset-1 bg-emerald-500/20 rounded-xl blur" />
|
||||
<div
|
||||
class="relative p-3 rounded-xl bg-gradient-to-br from-emerald-500 to-emerald-600 text-white shadow-lg shadow-emerald-500/30">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -92,13 +89,12 @@
|
||||
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-purple-600 to-pink-600 rounded-2xl blur opacity-0 group-hover:opacity-20 transition duration-300">
|
||||
</div>
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-purple-600 to-pink-600 rounded-2xl blur opacity-0 group-hover:opacity-20 transition duration-300" />
|
||||
<div
|
||||
class="relative bg-white dark:bg-slate-900 p-6 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm hover:shadow-md transition-all duration-300">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative">
|
||||
<div class="absolute -inset-1 bg-purple-500/20 rounded-xl blur"></div>
|
||||
<div class="absolute -inset-1 bg-purple-500/20 rounded-xl blur" />
|
||||
<div
|
||||
class="relative p-3 rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 text-white shadow-lg shadow-purple-500/30">
|
||||
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -124,8 +120,7 @@
|
||||
<!-- Search Bar -->
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl blur opacity-0 group-hover:opacity-10 transition duration-300">
|
||||
</div>
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl blur opacity-0 group-hover:opacity-10 transition duration-300" />
|
||||
<div
|
||||
class="relative bg-white dark:bg-slate-900 p-1.5 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 hover:border-blue-300 dark:hover:border-blue-700/50 transition-all">
|
||||
<div class="flex items-center gap-3 px-4">
|
||||
@@ -142,8 +137,7 @@
|
||||
<!-- Users Table -->
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl blur opacity-0 group-hover:opacity-5 transition duration-500">
|
||||
</div>
|
||||
class="absolute -inset-0.5 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl blur opacity-0 group-hover:opacity-5 transition duration-500" />
|
||||
<div
|
||||
class="relative bg-white dark:bg-slate-900 rounded-2xl shadow-lg border border-slate-200 dark:border-slate-800 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
@@ -188,8 +182,7 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-full blur opacity-0 group-hover/row:opacity-30 transition duration-300">
|
||||
</div>
|
||||
class="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-full blur opacity-0 group-hover/row:opacity-30 transition duration-300" />
|
||||
<div
|
||||
class="relative w-11 h-11 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 text-white flex items-center justify-center font-bold text-sm shadow-md shadow-blue-500/20">
|
||||
{{ getInitials(user.name) }}
|
||||
@@ -223,16 +216,16 @@
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-semibold shadow-sm border"
|
||||
:class="user.is_active ? 'bg-emerald-100 border-emerald-200 text-emerald-700 dark:bg-emerald-900/30 dark:border-emerald-800 dark:text-emerald-400' : 'bg-red-100 border-red-200 text-red-700 dark:bg-red-900/30 dark:border-red-800 dark:text-red-400'">
|
||||
<span class="w-1.5 h-1.5 rounded-full"
|
||||
:class="user.is_active ? 'bg-emerald-500' : 'bg-red-500'"></span>
|
||||
:class="user.is_active ? 'bg-emerald-500' : 'bg-red-500'" />
|
||||
{{ user.is_active ? 'Active' : 'Suspended' }}
|
||||
</span>
|
||||
</td>
|
||||
<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 @click.stop="openEditModal(user)"
|
||||
<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">
|
||||
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"
|
||||
@@ -240,9 +233,9 @@
|
||||
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 @click.stop="confirmDelete(user)"
|
||||
<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">
|
||||
title="Delete" @click.stop="confirmDelete(user)">
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
@@ -264,8 +257,7 @@
|
||||
<Transition enter-active-class="duration-300 ease-out" enter-from-class="opacity-0" enter-to-class="opacity-100"
|
||||
leave-active-class="duration-200 ease-in" leave-from-class="opacity-100" leave-to-class="opacity-0">
|
||||
<div v-if="showModal" class="fixed inset-0 z-[100] overflow-y-auto" role="dialog" aria-modal="true">
|
||||
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" @click="closeModal">
|
||||
</div>
|
||||
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" @click="closeModal" />
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<Transition enter-active-class="duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95 translate-y-4"
|
||||
@@ -274,8 +266,7 @@
|
||||
leave-to-class="opacity-0 scale-95 translate-y-4">
|
||||
<div class="relative w-full max-w-2xl">
|
||||
<div
|
||||
class="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-3xl blur opacity-20">
|
||||
</div>
|
||||
class="absolute -inset-1 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-3xl blur opacity-20" />
|
||||
<div
|
||||
class="relative overflow-hidden rounded-2xl bg-white dark:bg-slate-900 shadow-2xl border border-slate-200 dark:border-slate-800">
|
||||
|
||||
@@ -283,14 +274,13 @@
|
||||
class="relative bg-gradient-to-r from-slate-50 to-slate-100/50 dark:from-slate-800/50 dark:to-slate-800/30 px-8 py-6 border-b border-slate-200 dark:border-slate-800 flex justify-between items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-1.5 h-8 bg-gradient-to-b from-blue-600 to-indigo-600 rounded-full">
|
||||
</div>
|
||||
class="w-1.5 h-8 bg-gradient-to-b from-blue-600 to-indigo-600 rounded-full" />
|
||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white">{{ isEditing ?
|
||||
'Edit User Details' : 'Create New User' }}</h3>
|
||||
</div>
|
||||
<button @click="closeModal"
|
||||
class="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"><svg
|
||||
class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<button class="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
|
||||
@click="closeModal"><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="M6 18L18 6M6 6l12 12" />
|
||||
</svg></button>
|
||||
@@ -355,8 +345,7 @@
|
||||
<input v-model="formData.is_active" type="checkbox"
|
||||
class="sr-only peer">
|
||||
<div
|
||||
class="w-11 h-6 bg-slate-200 rounded-full peer peer-checked:bg-blue-600 dark:bg-slate-700 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all">
|
||||
</div>
|
||||
class="w-11 h-6 bg-slate-200 rounded-full peer peer-checked:bg-blue-600 dark:bg-slate-700 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all" />
|
||||
</div>
|
||||
<span class="text-sm font-semibold transition-colors"
|
||||
:class="formData.is_active ? 'text-slate-900 dark:text-white' : 'text-slate-500'">{{
|
||||
@@ -366,8 +355,9 @@
|
||||
</div>
|
||||
|
||||
<div v-if="isEditing" class="pt-2 border-t border-slate-100 dark:border-slate-800">
|
||||
<button type="button" @click="sendPasswordRecovery"
|
||||
class="text-sm text-red-500 hover:text-red-600 font-semibold flex items-center gap-2">
|
||||
<button type="button"
|
||||
class="text-sm text-red-500 hover:text-red-600 font-semibold flex items-center gap-2"
|
||||
@click="sendPasswordRecovery">
|
||||
<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="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
@@ -379,11 +369,13 @@
|
||||
|
||||
<div
|
||||
class="bg-gradient-to-r from-slate-50 to-slate-100/50 dark:from-slate-800/50 dark:to-slate-800/30 px-8 py-6 border-t border-slate-200 dark:border-slate-800 flex flex-row-reverse gap-3">
|
||||
<button @click="handleSubmit"
|
||||
class="px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-xl shadow-lg transition-all">{{
|
||||
<button
|
||||
class="px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-xl shadow-lg transition-all"
|
||||
@click="handleSubmit">{{
|
||||
isEditing ? 'Save Changes' : 'Create Account' }}</button>
|
||||
<button @click="closeModal"
|
||||
class="px-8 py-3 bg-white dark:bg-slate-800 border-2 border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold rounded-xl transition-all">Cancel</button>
|
||||
<button
|
||||
class="px-8 py-3 bg-white dark:bg-slate-800 border-2 border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold rounded-xl transition-all"
|
||||
@click="closeModal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,7 +389,7 @@
|
||||
leave-active-class="duration-200 ease-in" leave-from-class="opacity-100" leave-to-class="opacity-0">
|
||||
<div v-if="showDetailsModal" class="fixed inset-0 z-[100] overflow-y-auto" role="dialog" aria-modal="true">
|
||||
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"
|
||||
@click="closeDetailsModal"></div>
|
||||
@click="closeDetailsModal" />
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<Transition enter-active-class="duration-300 ease-out"
|
||||
enter-from-class="opacity-0 scale-95 translate-y-4"
|
||||
@@ -427,9 +419,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="closeDetailsModal"
|
||||
class="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"><svg
|
||||
class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<button class="p-2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300"
|
||||
@click="closeDetailsModal"><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="M6 18L18 6M6 6l12 12" />
|
||||
</svg></button>
|
||||
@@ -470,7 +462,7 @@
|
||||
class="px-2.5 py-1 rounded-md text-xs font-bold uppercase tracking-wide bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300">{{
|
||||
item.action.replace('_', ' ') }}</span>
|
||||
<span class="text-xs text-slate-400 font-medium">{{ item.target_type
|
||||
}} #{{ item.target_id }}</span>
|
||||
}} #{{ item.target_id }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-slate-700 dark:text-slate-300 leading-relaxed">{{
|
||||
item.details }}</p>
|
||||
@@ -482,8 +474,9 @@
|
||||
|
||||
<div
|
||||
class="flex-none bg-gradient-to-r from-slate-50 to-slate-100/50 dark:from-slate-800/50 dark:to-slate-800/30 px-8 py-4 border-t border-slate-200 dark:border-slate-800">
|
||||
<button @click="closeDetailsModal"
|
||||
class="w-full px-6 py-3 bg-white dark:bg-slate-800 border-2 border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold rounded-xl transition-all">Close</button>
|
||||
<button
|
||||
class="w-full px-6 py-3 bg-white dark:bg-slate-800 border-2 border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 font-semibold rounded-xl transition-all"
|
||||
@click="closeDetailsModal">Close</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ async function handleResetRequest() {
|
||||
|
||||
<div
|
||||
class="w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-lg p-8 border dark:border-gray-800 transition-colors">
|
||||
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100! mb-8 text-center">Reset Password</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 mb-8 text-center">Reset Password</h1>
|
||||
|
||||
<form class="space-y-6" @submit.prevent="handleResetRequest">
|
||||
<div>
|
||||
@@ -69,7 +69,7 @@ async function handleResetRequest() {
|
||||
'w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white',
|
||||
error ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
||||
]" placeholder="[email protected]">
|
||||
<p v-if="error" class="mt-1.5 text-xs text-red-500! ml-1 font-medium">{{ error }}</p>
|
||||
<p v-if="error" class="mt-1.5 text-xs text-red-500 ml-1 font-medium">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-4 pt-2">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<title> Login - Reddit for Academic Papers</title>
|
||||
<div
|
||||
class="min-h-screen bg-gray-100 dark:bg-gray-950 flex flex-col items-center justify-center p-6 transition-colors duration-300">
|
||||
<Transition appear enter-active-class="transition duration-700 ease-out" enter-from-class="translate-y-8 opacity-0"
|
||||
@@ -7,7 +6,7 @@
|
||||
|
||||
<div
|
||||
class="w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-lg p-8 border dark:border-gray-800 transition-colors">
|
||||
<h1 class="text-3xl font-bold text-gray-800 dark:text-white! mb-8 text-center capitalize">Login</h1>
|
||||
<h1 class="text-3xl font-bold text-gray-800 dark:text-white mb-8 text-center capitalize">Login</h1>
|
||||
|
||||
<div v-if="globalError"
|
||||
class="mb-6 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm text-center">
|
||||
@@ -21,7 +20,7 @@
|
||||
'w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white',
|
||||
errors.email ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
||||
]" placeholder="[email protected]">
|
||||
<p v-if="errors.email" class="mt-1 text-xs text-red-500! ml-1">{{ errors.email }}</p>
|
||||
<p v-if="errors.email" class="mt-1 text-xs text-red-500 ml-1">{{ errors.email }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -30,7 +29,7 @@
|
||||
'w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white',
|
||||
errors.password ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
||||
]" placeholder="••••••••" @keyup.enter="handleLogin">
|
||||
<p v-if="errors.password" class="mt-1 text-xs text-red-500! ml-1">{{ errors.password }}</p>
|
||||
<p v-if="errors.password" class="mt-1 text-xs text-red-500 ml-1">{{ errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-4 pt-2">
|
||||
|
||||
+470
-41
@@ -1,48 +1,477 @@
|
||||
<template>
|
||||
<title> Reddit for Academic Papers - Home</title>
|
||||
<div
|
||||
class="min-h-screen bg-gray-50 dark:bg-gray-950 flex items-center justify-center p-6 transition-colors duration-300">
|
||||
<div class="w-full max-w-2xl animate-[fadeInUp_0.6s_ease-out]">
|
||||
class="min-h-screen bg-gray-100 dark:bg-gray-950 text-gray-900 dark:text-gray-100 transition-colors duration-300">
|
||||
<div class="max-w-6xl mx-auto flex gap-6 py-8 px-4">
|
||||
|
||||
<header class="text-center mb-12">
|
||||
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 dark:text-white! mb-4 tracking-tight">
|
||||
Academic Management System
|
||||
</h1>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-md mx-auto">
|
||||
Streamline your educational workflow, manage student records, and oversee courses in one place.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<nav class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
|
||||
<nuxt-link to="/auth/login"
|
||||
class="no-underline text-current block group p-8 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div class="text-3xl mb-4 group-hover:scale-110 transition-transform">🔐</div>
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100! mb-2">Access Portal</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Sign in to manage your personalized dashboard and academic
|
||||
data.</p>
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link to="/auth-test"
|
||||
class="no-underline text-current block group p-8 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div class="text-3xl mb-4 group-hover:scale-110 transition-transform">🛠️</div>
|
||||
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100! mb-2">System Test</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Verify API connections and authentication state
|
||||
persistence.</p>
|
||||
</nuxt-link>
|
||||
|
||||
</nav>
|
||||
|
||||
<footer class="mt-16 text-center">
|
||||
<div
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-gray-200 dark:bg-gray-800 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
|
||||
</span>
|
||||
System Operational
|
||||
<aside v-if="!selectedPost" class="hidden lg:block w-fit min-w-[160px] py-24">
|
||||
<div class="sticky top-8">
|
||||
<div class="flex flex-col gap-2">
|
||||
<button v-for="tag in availableTags" :key="tag.id" :class="[
|
||||
'px-3 py-2 rounded-lg text-xs font-bold transition-all text-left shadow-sm cursor-pointer',
|
||||
subscribedTags.includes(tag.id)
|
||||
? `${tag.color} border-current ring-1 ring-inset ring-black/5`
|
||||
: 'bg-gray-50 dark:bg-gray-800 dark:border-gray-800 border opacity-60 hover:opacity-100'
|
||||
]" @click="toggleTagSubscription(tag)">
|
||||
<span class="opacity-70 mr-1">{{ subscribedTags.includes(tag.id) ? '✓' : '#' }}</span>
|
||||
{{ tag.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</aside>
|
||||
|
||||
<main :class="[
|
||||
'py-8 flex-1 transition-all duration-500 ease-in-out',
|
||||
selectedPost ? 'max-w-3xl mx-auto' : 'max-w-2xl'
|
||||
]">
|
||||
|
||||
<div v-show="!selectedPost" class="space-y-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>
|
||||
|
||||
<button
|
||||
class="w-10 h-10 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg transition-all active:scale-95 flex items-center justify-center group"
|
||||
title="New Post" @click="isModalOpen = true">
|
||||
<svg viewBox="0 0 24 24" class="w-5 h-5 fill-none stroke-current stroke-[2.5]"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="filteredPosts.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">
|
||||
|
||||
<div
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-48 h-48 bg-blue-500/10 dark:bg-blue-400/10 rounded-full blur-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
|
||||
|
||||
<div class="relative z-10 flex flex-col items-center">
|
||||
<div
|
||||
class="mb-4 p-4 bg-white dark:bg-slate-800 rounded-full shadow-sm ring-1 ring-slate-100 dark:ring-slate-700 group-hover:scale-110 group-hover:ring-blue-200 dark:group-hover:ring-blue-800 transition-all duration-300">
|
||||
<svg class="w-8 h-8 text-blue-500 dark:text-blue-400" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-bold text-slate-900 dark:text-white mb-1">
|
||||
No publications yet
|
||||
</h3>
|
||||
|
||||
<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"
|
||||
@click="isModalOpen = true">
|
||||
<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="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Submit your first post
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<TransitionGroup v-else appear enter-active-class="transition duration-500 ease-out"
|
||||
enter-from-class="translate-y-4 opacity-0" enter-to-class="translate-y-0 opacity-100">
|
||||
<div v-for="post in filteredPosts" :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" @click.stop="post.rating++">▲</button>
|
||||
<span class="text-xs font-bold">{{ post.rating }}</span>
|
||||
<button class="hover:text-red-500 transition-colors text-lg" @click.stop="post.rating--">▼</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>
|
||||
</TransitionGroup>
|
||||
|
||||
<div v-if="loading && filteredPosts.length === 0" class="space-y-4">
|
||||
<div v-for="i in 6" :key="i"
|
||||
class="bg-white dark:bg-gray-900 border dark:border-gray-800 rounded-xl p-5 animate-pulse">
|
||||
<div class="flex gap-4">
|
||||
<div class="w-10 h-16 bg-gray-100 dark:bg-gray-800 rounded-lg" />
|
||||
<div class="flex-1 space-y-3">
|
||||
<div class="h-3 w-24 bg-gray-100 dark:bg-gray-800 rounded" />
|
||||
<div class="h-5 w-3/4 bg-gray-100 dark:bg-gray-800 rounded" />
|
||||
<div class="h-3 w-full bg-gray-100 dark:bg-gray-800 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="loadMoreTrigger" class="h-20 w-full flex flex-col items-center justify-center gap-2">
|
||||
<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-xl p-8 shadow-lg">
|
||||
<header class="mb-6">
|
||||
<h1 class="text-3xl font-extrabold text-gray-900 dark:text-white mb-4 leading-tight">{{ selectedPost.title
|
||||
}}</h1>
|
||||
<div class="flex items-center gap-3 text-sm">
|
||||
<span class="bg-blue-600 text-white px-3 py-1 rounded-md font-bold text-xs uppercase">{{
|
||||
selectedPost.type
|
||||
}}</span>
|
||||
<span class="text-gray-500 font-medium">By <span class="text-blue-500">{{ selectedPost.submitter.name
|
||||
}}</span></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="prose dark:prose-invert max-w-none mb-8">
|
||||
<h4 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-2">Abstract</h4>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-lg">{{ selectedPost.abstract }}</p>
|
||||
</div>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 mb-8">
|
||||
|
||||
<section class="mb-10">
|
||||
<h3 class="text-xl font-bold mb-4">Comments ({{ selectedPost.comments.length }})</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-for="comment in selectedPost.comments" :key="comment.id" class="flex gap-4">
|
||||
<div class="w-0.5 bg-gray-200 dark:bg-gray-800 rounded-full ml-2" />
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-bold text-sm text-blue-600 dark:text-blue-400">{{ comment.author.name }}</span>
|
||||
<span class="text-[10px] text-gray-500 font-bold 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>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
<CreatePostModal :is-open="isModalOpen" :api="api" :token="token" @close="closeModal()"
|
||||
@refresh="handleRefresh" />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { useAuthStore } from "~/stores/auth-store.js";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute } from 'vue-router';
|
||||
import CreatePostModal from "~/components/ui/modal/CreatePostModal.vue";
|
||||
|
||||
const route = useRoute(); // 2. Access the route object
|
||||
const config = useRuntimeConfig();
|
||||
const authStore = useAuthStore();
|
||||
const { user, token } = storeToRefs(authStore);
|
||||
const api = config.public.apiBase;
|
||||
|
||||
// --- State Management ---
|
||||
const posts = ref([]);
|
||||
const selectedPost = ref(null);
|
||||
const loading = ref(false);
|
||||
const loadMoreTrigger = ref(null);
|
||||
const isModalOpen = ref(false);
|
||||
const searchQuery = ref('');
|
||||
const page = ref(1);
|
||||
|
||||
const sortBy = ref('date');
|
||||
const sortDesc = ref(true);
|
||||
|
||||
const subscribedTags = ref([]);
|
||||
const availableTags = ref([]);
|
||||
|
||||
const filterType = ref('ALL');
|
||||
|
||||
const newCommentContent = ref('');
|
||||
|
||||
// Form States
|
||||
|
||||
const checkQueryForModal = () => {
|
||||
if (route.query.openCreatePostModal === 'true') {
|
||||
isModalOpen.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
const router = useRouter();
|
||||
isModalOpen.value = false;
|
||||
router.replace({ query: { ...route.query, openCreatePostModal: undefined } });
|
||||
};
|
||||
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
const allTags = await $fetch(`${api}/tags`, {
|
||||
headers: { Authorization: `Bearer ${token.value}` }
|
||||
});
|
||||
|
||||
const colors = [
|
||||
"bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400",
|
||||
"bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400",
|
||||
"bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400",
|
||||
"bg-pink-100 dark:bg-pink-900/30 text-pink-700 dark:text-pink-400",
|
||||
"bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400"
|
||||
];
|
||||
|
||||
availableTags.value = allTags.map((tag, index) => ({
|
||||
...tag,
|
||||
color: colors[index % colors.length]
|
||||
}));
|
||||
|
||||
|
||||
const myTags = await $fetch(`${api}/tags/me`, {
|
||||
headers: { Authorization: `Bearer ${token.value}` }
|
||||
});
|
||||
|
||||
subscribedTags.value = myTags.map(t => t.id);
|
||||
} catch (err) {
|
||||
console.error("Error fetching tags:", err);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
const toggleTagSubscription = async (tag) => {
|
||||
const index = subscribedTags.value.indexOf(tag.id);
|
||||
const isSubscribing = index === -1;
|
||||
|
||||
if (isSubscribing) subscribedTags.value.push(tag.id);
|
||||
else subscribedTags.value.splice(index, 1);
|
||||
|
||||
try {
|
||||
const endpoint = isSubscribing
|
||||
? `${api}/tags/${tag.id}/subscribe`
|
||||
: `${api}/tags/${tag.id}/unsubscribe`;
|
||||
|
||||
await $fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token.value}` }
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
if (isSubscribing) subscribedTags.value.pop();
|
||||
else subscribedTags.value.push(tag.id);
|
||||
|
||||
console.error("Sync failed:", err);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
const handleRefresh = () => {
|
||||
isModalOpen.value = false;
|
||||
loadPosts();
|
||||
};
|
||||
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
const data = await $fetch(`${api}/posts`, {
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
if (data && data.length > 0) {
|
||||
posts.value = [...posts.value, ...data];
|
||||
page.value++;
|
||||
} else {
|
||||
hasMore.value = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch posts:", err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const fetchPostDetail = async (id) => {
|
||||
loading.value = true;
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
selectedPost.value = {
|
||||
id,
|
||||
title: posts.value.find(p => p.id === id)?.title || "A New Technique for Data Science",
|
||||
authors: ["Joana B", "External Researcher"],
|
||||
publication_date: "2025-11-01",
|
||||
type: "CONFERENCE_PAPER",
|
||||
abstract: "This breakthrough research explores the boundaries of predictive modeling and machine learning in academic environments. By leveraging sparse data sets, we achieve higher accuracy in student retention predictions.",
|
||||
submitter: { id: 2, name: "Joana B" },
|
||||
comments: [
|
||||
{ id: 101, content: "This technique could revolutionize our approach to Project X!", author: { name: "Joana B" }, created_at: "2025-11-10T10:30:00Z" },
|
||||
]
|
||||
};
|
||||
loading.value = false;
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const filteredPosts = computed(() => {
|
||||
const result = posts.value.filter(p => {
|
||||
const matchesType = filterType.value === 'ALL' || p.type === filterType.value;
|
||||
const matchesTags = subscribedTags.value.length === 0 ||
|
||||
p.tags.some(t => subscribedTags.value.includes(t.id));
|
||||
return matchesType && matchesTags;
|
||||
});
|
||||
|
||||
return result.sort((a, b) => {
|
||||
const modifier = sortDesc.value ? 1 : -1;
|
||||
|
||||
if (sortBy.value === 'votes') {
|
||||
return (b.rating - a.rating) * modifier;
|
||||
} else if (sortBy.value === 'comments') {
|
||||
return (b.comment_count - a.comment_count) * modifier;
|
||||
} else {
|
||||
return (new Date(b.publication_date).getTime() - new Date(a.publication_date).getTime()) * modifier;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
watch(posts, () => {
|
||||
if (filteredPosts.value.length < 3 && !loading.value && !selectedPost.value) {
|
||||
loadPosts();
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
watch([sortBy, sortDesc, filterType], () => {
|
||||
posts.value = [];
|
||||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
loadPosts();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
checkQueryForModal();
|
||||
|
||||
await Promise.all([
|
||||
loadPosts(),
|
||||
fetchTags()
|
||||
]);
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting && !loading.value && !selectedPost.value) {
|
||||
loadPosts();
|
||||
}
|
||||
}, {
|
||||
rootMargin: '300px',
|
||||
threshold: 0.1
|
||||
});
|
||||
|
||||
if (loadMoreTrigger.value) {
|
||||
observer.observe(loadMoreTrigger.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes fadeInUp {
|
||||
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
+20
-20
@@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<title>{{ userProfile.name }} - Reddit for Academic Papers</title>
|
||||
<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">
|
||||
|
||||
@@ -15,12 +14,11 @@
|
||||
<div
|
||||
class="h-32 w-32 rounded-full border-4 border-white dark:border-slate-800 bg-slate-100 dark:bg-slate-800 overflow-hidden shadow-md ring-1 ring-slate-200 dark:ring-slate-700">
|
||||
<img :src="userProfile.avatarUrl" :alt="userProfile.name"
|
||||
class="h-full w-full object-cover transition-transform hover:scale-105" />
|
||||
class="h-full w-full object-cover transition-transform hover:scale-105">
|
||||
</div>
|
||||
<div class="absolute bottom-2 right-2 h-5 w-5 rounded-full border-[3px] border-white dark:border-slate-900 shadow-sm"
|
||||
:class="userProfile.is_active ? 'bg-emerald-500' : 'bg-gray-400'"
|
||||
title="Online Status">
|
||||
</div>
|
||||
title="Online Status" />
|
||||
</div>
|
||||
|
||||
<h1 class="text-2xl font-bold text-slate-900 dark:text-white tracking-tight">{{
|
||||
@@ -33,8 +31,8 @@
|
||||
|
||||
<div class="w-full space-y-3">
|
||||
<button
|
||||
@click=" activeTab = 'settings' "
|
||||
class="w-full py-2.5 px-4 bg-slate-900 dark:bg-blue-600 hover:bg-slate-800 dark:hover:bg-blue-500 text-white font-medium rounded-lg text-sm transition-all shadow-sm active:scale-95 cursor-pointer">
|
||||
class="w-full py-2.5 px-4 bg-slate-900 dark:bg-blue-600 hover:bg-slate-800 dark:hover:bg-blue-500 text-white font-medium rounded-lg text-sm transition-all shadow-sm active:scale-95 cursor-pointer"
|
||||
@click=" activeTab = 'settings'">
|
||||
Edit Profile
|
||||
</button>
|
||||
</div>
|
||||
@@ -81,7 +79,7 @@
|
||||
class="bg-white dark:bg-slate-900 p-4 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 text-center hover:border-slate-300 dark:hover:border-slate-700 transition-colors">
|
||||
<div class="text-2xl font-bold text-slate-900 dark:text-white">{{
|
||||
userProfile.publications.length
|
||||
}}</div>
|
||||
}}</div>
|
||||
<div
|
||||
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-1">
|
||||
Papers
|
||||
@@ -94,19 +92,22 @@
|
||||
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 p-1.5 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm flex gap-1 overflow-x-auto transition-colors">
|
||||
<button @click="activeTab = 'overview'"
|
||||
<button
|
||||
:class="activeTab === 'overview' ? 'bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm ring-1 ring-black/5 dark:ring-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-50 dark:hover:bg-slate-800'"
|
||||
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-semibold cursor-pointer transition-colors">
|
||||
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-semibold cursor-pointer transition-colors"
|
||||
@click="activeTab = 'overview'">
|
||||
Overview
|
||||
</button>
|
||||
<button @click="activeTab = 'activity'"
|
||||
<button
|
||||
:class="activeTab === 'activity' ? 'bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm ring-1 ring-black/5 dark:ring-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-50 dark:hover:bg-slate-800'"
|
||||
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-medium transition-colors cursor-pointer">
|
||||
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-medium transition-colors cursor-pointer"
|
||||
@click="activeTab = 'activity'">
|
||||
Activity Log
|
||||
</button>
|
||||
<button @click="activeTab = 'settings'"
|
||||
<button
|
||||
:class="activeTab === 'settings' ? 'bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm ring-1 ring-black/5 dark:ring-white/5' : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-50 dark:hover:bg-slate-800'"
|
||||
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-medium transition-colors cursor-pointer">
|
||||
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-medium transition-colors cursor-pointer"
|
||||
@click="activeTab = 'settings'">
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
@@ -143,8 +144,7 @@
|
||||
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">
|
||||
|
||||
<div
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-48 h-48 bg-blue-500/10 dark:bg-blue-400/10 rounded-full blur-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none">
|
||||
</div>
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-48 h-48 bg-blue-500/10 dark:bg-blue-400/10 rounded-full blur-3xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
|
||||
|
||||
<div class="relative z-10 flex flex-col items-center">
|
||||
<div
|
||||
@@ -166,8 +166,8 @@
|
||||
</p>
|
||||
|
||||
<button
|
||||
@click="goToCreatePost()"
|
||||
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="goToCreatePost()">
|
||||
<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="M12 4v16m8-8H4" />
|
||||
@@ -181,7 +181,7 @@
|
||||
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>
|
||||
}}</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 }}
|
||||
@@ -255,8 +255,8 @@
|
||||
<div v-else-if="activeTab === 'settings'" key="settings"
|
||||
class="bg-white dark:bg-slate-900 p-6 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-800 transition-colors">
|
||||
<h2
|
||||
class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-3">
|
||||
Settings</h2>
|
||||
class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-3">
|
||||
Settings</h2>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Nothing here but chickens! Settings coming soon.
|
||||
</p>
|
||||
|
||||
@@ -59,7 +59,7 @@ async function handleUpdatePassword() {
|
||||
|
||||
<div
|
||||
class="w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-lg p-8 border dark:border-gray-800 transition-colors">
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white! mb-2 text-center">Password Update</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white mb-2 text-center">Password Update</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-8 text-center">Please update your password to
|
||||
secure your account.</p>
|
||||
|
||||
|
||||
+7
-1
@@ -3,7 +3,13 @@ import tailwindcss from "@tailwindcss/vite";
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2026-01-01",
|
||||
devtools: { enabled: true },
|
||||
|
||||
app: {
|
||||
head: {
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
||||
]
|
||||
}
|
||||
},
|
||||
css: [
|
||||
"~/assets/css/main.css",
|
||||
"~/assets/css/tailwind.css"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 401 KiB |
Reference in New Issue
Block a user