home-page functionality, and some design tweaks #10

Merged
FernandoJVideira merged 1 commits from feature/home-page into develop 2026-01-21 02:10:54 +00:00
12 changed files with 889 additions and 170 deletions
+1
View File
@@ -4,6 +4,7 @@ import Toast from './components/ui/toast/Toast.vue';
<template>
<NuxtLayout>
<Title>Reddit for scientists</Title>
<Toast />
<NuxtPage />
</NuxtLayout>
+1 -6
View File
@@ -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 {
+301
View File
@@ -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
View File
@@ -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>
+42 -49
View File
@@ -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>
+2 -2
View File
@@ -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">
+3 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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
View File
@@ -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