Author SHA1 Message Date
AfonsoCMSousa 951bed6fbf feat: Base for the actuall profile page. 2026-01-15 19:01:28 +00:00
11 changed files with 447 additions and 408 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
<template> <template>
<NuxtLayout> <div>
<NuxtPage /> <NuxtPage />
</NuxtLayout> </div>
</template> </template>
+171
View File
@@ -0,0 +1,171 @@
:root {
--text-primary: #000000;
--text-secondary: #8d96a0;
--text-tertiary: #6e7681;
--text-link: #2f81f7;
--text-link-hover: #539bf5;
--border-primary: #30363d;
--border-hover: #8d96a0;
--accent: #7aa2f7;
--accent-hover: #9ab6ff;
--accent-primary: #2f81f7;
--accent-emphasis: #539bf5;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
/* Typography */
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI",
"Noto Sans", Helvetica, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
/* Radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
}
body {
font-family: var(--font-sans);
color: var(--text-primary);
background-color: var(--bg-primary);
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.25;
color: var(--text-primary);
}
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;
}
code {
font-family: var(--font-mono);
background: var(--bg-tertiary);
padding: 0.2em 0.4em;
border-radius: var(--radius-sm);
}
SEMANTIC COMPONENTS (SAFE)
============================================== */
.card {
background: var(--bg-surface);
color: var(--text-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
}
.app-card {
background-color: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.app-card:hover {
border-color: var(--border-hover);
}
.app-btn {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 500;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.app-btn-primary {
background-color: var(--accent-primary);
color: #fff;
}
.app-btn-primary:hover {
background-color: var(--accent-emphasis);
}
.app-btn-secondary {
background-color: var(--bg-tertiary);
border: 1px solid var(--border-primary);
color: var(--text-primary);
}
.app-btn-secondary:hover {
background-color: var(--bg-hover);
}
.button {
background: var(--accent);
color: #fff;
border-radius: var(--radius-md);
transition: background var(--transition-fast);
}
.button:hover {
background: var(--accent-hover);
}
.app-badge {
display: inline-flex;
align-items: center;
padding: 0 0.5rem;
height: 1.25rem;
font-size: 0.75rem;
border-radius: var(--radius-full);
}
.app-badge-success {
color: var(--success);
background: color-mix(in srgb, var(--success) 15%, transparent);
}
.app-avatar {
border-radius: var(--radius-full);
overflow: hidden;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-primary);
}
+1 -1
View File
@@ -1,7 +1,7 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:is(.dark *));
@theme inline { @theme inline {
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
-68
View File
@@ -1,68 +0,0 @@
<script setup>
import { useAuthStore } from "~/stores/auth-store.js";
const authStore = useAuthStore();
const isDark = ref(false);
const router = useRouter();
function toggleDark() {
isDark.value = !isDark.value;
if (isDark.value) {
document.documentElement.classList.add('dark');
localStorage.theme = 'dark';
} else {
document.documentElement.classList.remove('dark');
localStorage.theme = 'light';
}
}
function logout() {
authStore.logout()
router.push("/auth/login");
}
onMounted(() => {
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
isDark.value = true;
document.documentElement.classList.add('dark');
}
});
</script>
<template>
<nav
class="sticky top-0 z-50 flex items-center justify-between px-6 py-4 bg-white/80 backdrop-blur-md shadow-sm border-b border-gray-100">
<div class="flex items-center gap-6">
<nuxt-link to="/" class="text-sm font-medium text-gray-600 hover:text-blue-600 transition-colors duration-200"
active-class="text-blue-600 font-bold">
Home
</nuxt-link>
</div>
<div class="flex items-center gap-4">
<button @click="toggleDark" class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700">
<span v-if="isDark"></span>
<span v-else>🌙</span>
</button>
<template v-if="authStore?.token">
<label class="py-2 text-sm font-semibold">{{ authStore?.user.name }}</label>
<button @click.prevent="logout"
class="px-4 py-2 text-sm font-semibold text-red-800 bg-red-100 hover:bg-red-200 rounded-lg transition-all duration-200">
Logout
</button>
</template>
<template v-else>
<nuxt-link to="/auth/login"
class="px-5 py-2 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-sm transition-all">
Login
</nuxt-link>
</template>
</div>
</nav>
<main class="max-w-7xl mx-auto">
<slot />
</main>
</template>
-84
View File
@@ -1,84 +0,0 @@
<template>
<div>
<h1>Login Form</h1>
<div>Username: <input v-model="loginFormData.username" /></div>
<div>Password: <input v-model="loginFormData.password" /></div>
<button @click="login">LOGIN</button>
<button @click="reset">RESET</button>
</div>
<div v-if="token">
<h2>API Request Form</h2>
<div>
Request: <code>GET {{ api }}</code>/ <input v-model="loginFormData.password" />
</div>
<div>Token: {{ token }}</div>
<button @click="sendRequest">SEND REQUEST</button>
</div>
<div v-if="messages.length > 0">
<h2>Messages</h2>
<div v-for="message in messages">
<pre>{{ message }}</pre>
</div>
</div>
</template>
<script setup>
const config = useRuntimeConfig();
const api = config.public.apiBase;
const loginFormData = reactive({
username: null,
password: null,
});
const token = ref(null);
const messages = ref([]);
async function login() {
reset();
try {
await $fetch(`${api}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: loginFormData,
onResponse({ request, response, options }) {
messages.value.push({
method: options.method,
request: request,
status: response.status,
statusText: response.statusText,
payload: response._data,
});
if (response.status == 200) token.value = response._data;
},
});
} catch (e) {
console.error("login request failed: ", e);
}
}
function reset() {
token.value = null;
messages.value = [];
}
async function sendRequest() {
try {
await $fetch(`${api}/`, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: `Bearer ${token.value}`,
},
onResponse({ request, response, options }) {
messages.value.push({
method: options.method,
request: request,
status: response.status,
statusText: response.statusText,
payload: response._data,
});
},
});
} catch (e) {
console.error("api request failed: ", e);
}
}
</script>
-147
View File
@@ -1,147 +0,0 @@
<template>
<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"
enter-to-class="translate-y-0 opacity-100">
<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 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">
{{ globalError }}
</div>
<div class="space-y-5">
<div>
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">Email</label>
<input v-model="loginFormData.email" type="email" :class="[
'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>
</div>
<div>
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">Password</label>
<input v-model="loginFormData.password" type="password" @keyup.enter="handleLogin" :class="[
'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="••••••••" />
<p v-if="errors.password" class="mt-1 text-xs text-red-500 ml-1">{{ errors.password }}</p>
</div>
<div class="flex justify-center pt-4">
<button @click="handleLogin" :disabled="loading"
class="w-1/2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-bold py-3 rounded-lg shadow-md hover:shadow-lg transform active:scale-95 transition-all duration-200">
{{ loading ? 'AUTHENTICATING...' : 'LOGIN' }}
</button>
</div>
</div>
</div>
</Transition>
</div>
</template>
<style scoped>
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fadeInUp 0.6s ease-out forwards;
}
.delay-200 {
animation-delay: 0.2s;
opacity: 0;
}
</style>
<script setup>
import { useAuthStore } from "~/stores/auth-store.js";
import { storeToRefs } from "pinia";
const config = useRuntimeConfig();
const api = config.public.apiBase;
const authStore = useAuthStore();
const router = useRouter();
const { token, user } = storeToRefs(authStore);
const loginFormData = reactive({
email: "",
password: "",
});
const errors = reactive({ email: null, password: null });
const globalError = ref(null);
const loading = ref(false);
function validateForm() {
errors.email = !loginFormData.email ? "Email is required" : null;
errors.password = !loginFormData.password ? "Password is required" : null;
if (loginFormData.email && !/^\S+@\S+\.\S+$/.test(loginFormData.email)) {
errors.email = "Please enter a valid email address";
}
return !errors.email && !errors.password;
}
async function handleLogin() {
globalError.value = null;
if (!validateForm()) return;
loading.value = true;
try {
await $fetch(`${api}/auth/login`, {
method: "POST",
body: loginFormData,
onResponse({ response }) {
if (response.status === 200) {
console.log(response)
token.value = response._data.token;
user.value = response._data.user;
router.push("/")
//getUserInfo();
}
},
onResponseError({ response }) {
console.log(response.status)
if (response.status === 401) {
globalError.value = "Invalid email or password.";
} else {
globalError.value = "An unexpected error occurred. Please try again.";
}
}
});
} catch (e) {
if (!globalError.value) globalError.value = "Network error: Connection to server failed.";
} finally {
loading.value = false;
}
}
async function getUserInfo() {
try {
const data = await $fetch(`${api}/auth/user`, {
method: "GET",
headers: {
Authorization: `Bearer ${token.value}`,
},
});
user.value = data;
} catch (e) {
console.error("User info fetch failed:", e);
}
}
</script>
+3 -45
View File
@@ -1,47 +1,5 @@
<template> <template>
<div <div>
class="min-h-screen bg-gray-50 dark:bg-gray-950 flex items-center justify-center p-6 transition-colors duration-300"> <h1> This will be the home page </h1>
<div class="w-full max-w-2xl animate-[fadeInUp_0.6s_ease-out]">
<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="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="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>
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
System Operational
</div>
</footer>
</div>
</div> </div>
</template> </template>
+240 -2
View File
@@ -1,5 +1,243 @@
<template> <template>
<div> <title> Profile - {{user.name}}</title>
<h1>This will be the profile page! HORRAY!</h1> <div class="min-h-screen bg-slate-50 font-sans text-slate-900 selection:bg-blue-100 pb-12">
<div class="container mx-auto max-w-7xl px-4 py-8 md:py-12">
<div class="grid grid-cols-1 md:grid-cols-12 gap-8 lg:gap-12">
<div class="md:col-span-4 lg:col-span-3 flex flex-col gap-6">
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-200">
<div class="flex flex-col items-center text-center">
<div class="relative inline-block mb-4">
<div
class="h-32 w-32 rounded-full border-4 border-white bg-slate-100 overflow-hidden shadow-md ring-1 ring-slate-200">
<img :src="user.avatarUrl" :alt="user.name"
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 shadow-sm"
:class="user.isActive ? 'bg-emerald-500' : 'bg-gray-400'" title="Online Status">
</div>
</div>
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">{{ user.name }}</h1>
<p class="text-sm font-semibold text-slate-500 mb-6 uppercase tracking-wide">{{ user.role }}
</p>
<div class="w-full space-y-3">
<button
class="w-full py-2.5 px-4 bg-slate-900 hover:bg-slate-800 text-white font-medium rounded-lg text-sm transition-all shadow-sm active:scale-95 cursor-pointer">
Edit Profile
</button>
<button
class="w-full py-2.5 px-4 bg-white border border-slate-300 hover:bg-slate-50 text-slate-700 font-medium rounded-lg text-sm transition-all active:scale-95 cursor-pointer">
View Settings
</button>
</div>
</div>
<div class="mt-8 pt-6 border-t border-slate-100 space-y-4">
<div class="flex items-center gap-3 text-sm text-slate-600">
<svg class="w-4 h-4 text-slate-400 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span class="truncate">{{ user.email }}</span>
</div>
<div class="flex items-center gap-3 text-sm text-slate-600">
<svg class="w-4 h-4 text-slate-400 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<span>Polytechnic of Leiria</span>
</div>
<div class="flex items-center gap-3 text-sm text-slate-600">
<svg class="w-4 h-4 text-slate-400 shrink-0" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>Joined {{ formattedDate }}</span>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div
class="bg-white p-4 rounded-xl shadow-sm border border-slate-200 text-center hover:border-slate-300 transition-colors">
<div class="text-2xl font-bold text-slate-900">{{ totalCitations }}</div>
<div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Citations
</div>
</div>
<div
class="bg-white p-4 rounded-xl shadow-sm border border-slate-200 text-center hover:border-slate-300 transition-colors">
<div class="text-2xl font-bold text-slate-900">{{ user.publications.length }}</div>
<div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Papers
</div>
</div>
</div>
</div>
<div class="md:col-span-8 lg:col-span-9 space-y-6">
<div class="bg-white p-1.5 rounded-xl border border-slate-200 shadow-sm flex gap-1 overflow-x-auto">
<button
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-semibold bg-slate-100 text-slate-900 shadow-sm ring-1 ring-black/5 cursor-pointer">
Overview
</button>
<button
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-medium text-slate-500 hover:text-slate-900 hover:bg-slate-50 transition-colors cursor-pointer">
Activity Log
</button>
<button
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-medium text-slate-500 hover:text-slate-900 hover:bg-slate-50 transition-colors cursor-pointer">
Settings
</button>
</div>
<div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
<h2 class="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3">Biography</h2>
<p class="text-slate-700 leading-relaxed text-sm">
{{ user.biography }}
</p>
</div>
<div class="flex items-center justify-between pt-2 px-1">
<h2 class="text-lg font-bold text-slate-900 flex items-center gap-2">
Recent Publications
<span class="bg-slate-200 text-slate-700 text-xs font-bold px-2 py-0.5 rounded-full">{{
user.publications.length }}</span>
</h2>
</div>
<div class="flex flex-col gap-4">
<div v-if="user.publications.length === 0"
class="text-center py-16 border-2 border-dashed border-slate-200 rounded-xl bg-slate-50/50">
<p class="text-slate-500 font-medium">No publications found yet.</p>
</div>
<div v-for="pub in user.publications" :key="pub.id"
class="bg-white p-6 rounded-xl shadow-sm border border-slate-200 hover:shadow-md hover:border-blue-300 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 group-hover:text-blue-600 transition-colors leading-snug">
<a href="#" class="before:absolute before:inset-0">{{ pub.title }}</a>
</h3>
<span
class="shrink-0 px-2.5 py-1 rounded-md bg-slate-100 text-slate-600 text-xs font-bold border border-slate-200">
{{ pub.publishedYear }}
</span>
</div>
<p class="text-slate-600 text-sm leading-relaxed line-clamp-2">
{{ pub.abstract }}
</p>
<div
class="mt-4 pt-4 border-t border-slate-50 flex flex-wrap items-center gap-4 text-xs font-medium z-10 relative">
<span
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-blue-50 text-blue-700 border border-blue-100">
<span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
{{ pub.field }}
</span>
<span class="inline-flex items-center gap-1.5 text-slate-500">
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
{{ pub.citations }} Citations
</span>
<span class="text-slate-300"></span>
<span class="text-slate-500">{{ pub.publishedDate }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script setup lang="ts">
import { ref, computed } from 'vue';
enum Role {
STUDENT = "STUDENT",
TEACHER = "TEACHER",
ADMIN = "ADMIN",
RESEARCHER = "RESEARCHER"
}
interface Publication {
id: number;
title: string;
abstract: string;
publishedDate: string;
publishedYear: string;
citations: number;
field: string;
}
interface User {
id: number;
name: string;
email: string;
role: Role;
isActive: boolean;
avatarUrl: string;
joinDate: string;
publications: Publication[];
}
const user = ref<User>({
id: 1,
name: 'Dr. Maria Santos',
email: '[email protected]',
biography: 'Focused on distributed systems and IoT security. Currently leading the research group on \"Smart City Infrastructures\" at IPLeiria. My work primarily involves developing scalable architectures for real-time data processing and ensuring data integrity in edge computing environments.',
role: Role.RESEARCHER,
isActive: true,
avatarUrl: 'https://api.dicebear.com/7.x/notionists/svg?seed=Maria',
joinDate: '2023-09-15',
publications: [
{
id: 1,
title: 'Machine Learning Approaches to Predictive Maintenance in Industrial IoT',
abstract: 'This paper explores novel machine learning techniques for predictive maintenance in industrial Internet of Things environments, demonstrating a 35% improvement in fault detection accuracy.',
publishedDate: 'Nov 20, 2024',
publishedYear: '2024',
citations: 12,
field: 'Computer Science'
},
{
id: 2,
title: 'Distributed Systems Architecture for Real-Time Data Processing',
abstract: 'We present a scalable distributed architecture designed for real-time processing of large-scale sensor data streams. The proposed system leverages Kafka for ingestion.',
publishedDate: 'Aug 05, 2024',
publishedYear: '2024',
citations: 8,
field: 'Software Engineering'
},
{
id: 3,
title: 'Security Considerations in Edge Computing Environments',
abstract: 'An in-depth analysis of security vulnerabilities in edge computing deployments. This paper proposes a layered security framework that addresses both hardware and software.',
publishedDate: 'Mar 12, 2024',
publishedYear: '2024',
citations: 23,
field: 'Cybersecurity'
}
]
});
const totalCitations = computed(() => user.value.publications.reduce((sum, pub) => sum + pub.citations, 0));
const formattedDate = computed(() => {
return new Date(user.value.joinDate).toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
});
});
</script>
-42
View File
@@ -1,42 +0,0 @@
import { defineStore } from "pinia";
export const useAuthStore = defineStore("authStore", () => {
const token = useCookie("auth_token", { maxAge: 60 * 15 });
const user = useCookie("auth_user", { maxAge: 60 * 15 });
if (process.client) {
token.value = localStorage.getItem("auth_token");
const savedUser = localStorage.getItem("auth_user");
if (savedUser) {
try {
user.value = JSON.parse(savedUser);
} catch (e) {
console.error("Failed to parse user from storage", e);
}
}
}
watch([token, user], ([newToken, newUser]) => {
if (!process.client) return;
if (newToken) {
localStorage.setItem("auth_token", newToken);
} else {
localStorage.removeItem("auth_token");
}
if (newUser) {
localStorage.setItem("auth_user", JSON.stringify(newUser));
} else {
localStorage.removeItem("auth_user");
}
}, { deep: true });
function logout() {
token.value = null;
user.value = null;
}
return { token, user, logout };
});
+29 -14
View File
@@ -1,20 +1,35 @@
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: "2026-01-01", compatibilityDate: "2025-07-15",
devtools: { enabled: true }, devtools: { enabled: true },
css: ["~/assets/css/tailwind.css"], css: [
"~/assets/css/main.css",
vite: { "~/assets/css/tailwind.css"
plugins: [tailwindcss()], ],
}, vite: {
plugins: [tailwindcss()],
runtimeConfig: {
public: {
apiBase: "http://localhost:8080/academics/api/v1",
}, },
},
modules: ["@pinia/nuxt"], modules: [
}); "@nuxt/fonts",
"@nuxt/icon",
"@nuxt/image",
"@nuxt/eslint",
"shadcn-nuxt",
],
shadcn: {
/**
* Prefix for all the imported component
*/
prefix: "",
/**
* Directory that the component lives in.
* @default "./app/components/ui"
*/
componentDir: "./app/components/ui",
},
});
-2
View File
@@ -20,9 +20,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^9.39.0", "eslint": "^9.39.0",
"lucide-vue-next": "^0.552.0", "lucide-vue-next": "^0.552.0",
"@pinia/nuxt": "0.11.3",
"nuxt": "^4.2.0", "nuxt": "^4.2.0",
"pinia": "^3.0.4",
"reka-ui": "^2.6.0", "reka-ui": "^2.6.0",
"shadcn-nuxt": "^2.3.2", "shadcn-nuxt": "^2.3.2",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",