Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f031dddc4c | ||
|
|
351b54d941 | ||
|
|
9c0dca8f13 | ||
|
|
7f26585649 | ||
|
|
0fe0b444c2 | ||
|
|
5132e3eb2f | ||
|
|
4c33975d1b | ||
|
|
8da82f2735 | ||
|
|
0dcd1cfcc4 | ||
|
|
6968631b86 | ||
|
|
85acc051b1 | ||
|
|
d8d7e3e38a | ||
|
|
88b51a6faa | ||
|
|
d8c262ff5f | ||
|
|
951bed6fbf |
@@ -1,5 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Toast from './components/ui/toast/Toast.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
|
<Title>Reddit for scientists</Title>
|
||||||
|
<Toast />
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
|
||||||
|
:root {
|
||||||
|
--text-primary: #000000;
|
||||||
|
--text-secondary: #8d96a0;
|
||||||
|
--text-tertiary: #6e7681;
|
||||||
|
--text-link: #2f81f7;
|
||||||
|
--text-link-hover: #539bf5;
|
||||||
|
--text-slate: #57606a;
|
||||||
|
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
const store = useNotificationStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed bottom-6 right-6 z-[9999] flex flex-col gap-3 w-80">
|
||||||
|
<TransitionGroup name="toast">
|
||||||
|
<div v-for="toast in store.notifications" :key="toast.id" :class="[
|
||||||
|
'p-4 rounded-lg shadow-xl border text-sm font-medium transition-all duration-300',
|
||||||
|
toast.type === 'error'
|
||||||
|
? 'bg-red-50 border-red-200 text-red-800'
|
||||||
|
: 'bg-green-50 border-green-200 text-green-800'
|
||||||
|
]">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>{{ toast.message }}</span>
|
||||||
|
<button class="ml-4 opacity-50 hover:opacity-100" @click="store.remove(toast.id)">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toast-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+192
-47
@@ -2,67 +2,212 @@
|
|||||||
import { useAuthStore } from "~/stores/auth-store.js";
|
import { useAuthStore } from "~/stores/auth-store.js";
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const isDark = ref(false);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// State (No more isScrolled)
|
||||||
|
const isDark = ref(false);
|
||||||
|
const isUserMenuOpen = ref(false);
|
||||||
|
const isMobileMenuOpen = ref(false);
|
||||||
|
const userMenuRef = ref(null);
|
||||||
|
|
||||||
|
// Toggle Dark Mode
|
||||||
function toggleDark() {
|
function toggleDark() {
|
||||||
isDark.value = !isDark.value;
|
isDark.value = !isDark.value;
|
||||||
if (isDark.value) {
|
if (isDark.value) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
localStorage.theme = 'dark';
|
localStorage.theme = 'dark';
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.remove('dark');
|
||||||
localStorage.theme = 'light';
|
localStorage.theme = 'light';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
// Navigation Links
|
||||||
authStore.logout()
|
const navLinks = [
|
||||||
router.push("/auth/login");
|
{ name: 'Dashboard', path: '/' },
|
||||||
|
{ name: 'Courses', path: '/courses' },
|
||||||
|
{ name: 'Students', path: '/students' },
|
||||||
|
{ name: 'Schedule', path: '/schedule' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
isUserMenuOpen.value = false;
|
||||||
|
isMobileMenuOpen.value = false;
|
||||||
|
authStore.logout();
|
||||||
|
router.push("/auth/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(path) {
|
||||||
|
isUserMenuOpen.value = false;
|
||||||
|
isMobileMenuOpen.value = false;
|
||||||
|
router.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDropdowns(event) {
|
||||||
|
if (userMenuRef.value && !userMenuRef.value.contains(event.target)) {
|
||||||
|
isUserMenuOpen.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
// Initialize Theme
|
||||||
isDark.value = true;
|
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
document.documentElement.classList.add('dark');
|
isDark.value = true;
|
||||||
}
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click listener for closing dropdowns
|
||||||
|
window.addEventListener('click', closeDropdowns);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('click', closeDropdowns);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav
|
<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">
|
class="sticky top-0 z-50 w-full border-b border-slate-200 dark:border-slate-800 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md transition-colors duration-300">
|
||||||
|
<div class="w-full px-6 md:px-8">
|
||||||
|
<div class="flex items-center justify-between h-16">
|
||||||
|
|
||||||
<div class="flex items-center gap-6">
|
<nuxt-link to="/" class="flex items-center gap-2 group flex-shrink-0">
|
||||||
<nuxt-link to="/" class="text-sm font-medium text-gray-600 hover:text-blue-600 transition-colors duration-200"
|
<div
|
||||||
active-class="text-blue-600 font-bold">
|
class="w-9 h-9 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold text-xl shadow-blue-500/20 shadow-lg group-hover:scale-110 transition-transform">
|
||||||
Home
|
A
|
||||||
</nuxt-link>
|
</div>
|
||||||
</div>
|
<span class="text-xl font-bold text-slate-900 dark:text-white tracking-tight hidden md:block">
|
||||||
|
Academic<span class="text-blue-600">Sys</span>
|
||||||
|
</span>
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4 md:gap-8">
|
||||||
<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">
|
<div class="hidden md:flex items-center gap-1">
|
||||||
<label class="py-2 text-sm font-semibold">{{ authStore?.user.name }}</label>
|
<nuxt-link v-for="link in navLinks" :key="link.path" :to="link.path"
|
||||||
<button @click.prevent="logout"
|
class="px-3 py-2 rounded-md text-sm font-medium 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"
|
||||||
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">
|
active-class="bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-semibold">
|
||||||
Logout
|
{{ link.name }}
|
||||||
</button>
|
</nuxt-link>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<div class="hidden md:block h-6 w-px bg-slate-200 dark:bg-slate-700 mx-2"></div>
|
||||||
<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">
|
<div class="flex items-center gap-3">
|
||||||
<slot />
|
|
||||||
</main>
|
<button @click="toggleDark"
|
||||||
|
class="p-2.5 rounded-full text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
<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" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else 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="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="authStore?.token" class="relative" ref="userMenuRef">
|
||||||
|
<button @click.stop="isUserMenuOpen = !isUserMenuOpen"
|
||||||
|
class="flex items-center gap-3 pl-2 pr-1 py-1.5 rounded-full border border-transparent hover:bg-slate-100 dark:hover:bg-slate-800 transition-all focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
:class="{ 'bg-slate-100 dark:bg-slate-800': isUserMenuOpen }">
|
||||||
|
<span
|
||||||
|
class="text-sm font-semibold text-slate-700 dark:text-slate-200 hidden sm:block ml-2">
|
||||||
|
{{ authStore.user.name.split(' ')[0] }}
|
||||||
|
</span>
|
||||||
|
<img :src="authStore.user.avatarUrl || `https://ui-avatars.com/api/?name=${authStore.user.name}&background=0D8ABC&color=fff`"
|
||||||
|
alt="Avatar"
|
||||||
|
class="w-9 h-9 rounded-full border border-slate-200 dark:border-slate-700 object-cover">
|
||||||
|
<svg class="w-4 h-4 text-slate-400" :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"
|
||||||
|
enter-from-class="transform opacity-0 scale-95"
|
||||||
|
enter-to-class="transform opacity-100 scale-100"
|
||||||
|
leave-active-class="transition ease-in duration-75"
|
||||||
|
leave-from-class="transform opacity-100 scale-100"
|
||||||
|
leave-to-class="transform opacity-0 scale-95">
|
||||||
|
<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-100 dark:border-slate-800 py-2 focus:outline-none z-50">
|
||||||
|
<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-medium">Signed in as
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-bold text-slate-900 dark:text-white truncate">{{
|
||||||
|
authStore.user.email }}</p>
|
||||||
|
</div>
|
||||||
|
<button @click="navigateTo('/profile')"
|
||||||
|
class="w-full text-left px-4 py-2.5 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-3 transition-colors">
|
||||||
|
<span>👤</span> Your Profile
|
||||||
|
</button>
|
||||||
|
<button @click="navigateTo('/settings')"
|
||||||
|
class="w-full text-left px-4 py-2.5 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-3 transition-colors">
|
||||||
|
<span>⚙️</span> Settings
|
||||||
|
</button>
|
||||||
|
<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 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-3 transition-colors">
|
||||||
|
<span>🚪</span> Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<nuxt-link to="/auth/login"
|
||||||
|
class="hidden sm:inline-flex items-center justify-center px-6 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-sm shadow-blue-500/30 transition-all">
|
||||||
|
Sign In
|
||||||
|
</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">
|
||||||
|
<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"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-7 h-7" 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>
|
||||||
|
|
||||||
|
<transition enter-active-class="transition duration-200 ease-out"
|
||||||
|
enter-from-class="transform -translate-y-4 opacity-0"
|
||||||
|
enter-to-class="transform translate-y-0 opacity-100"
|
||||||
|
leave-active-class="transition duration-150 ease-in"
|
||||||
|
leave-from-class="transform translate-y-0 opacity-100"
|
||||||
|
leave-to-class="transform -translate-y-4 opacity-0">
|
||||||
|
<div v-if="isMobileMenuOpen"
|
||||||
|
class="md:hidden border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-xl">
|
||||||
|
<div class="px-4 pt-2 pb-6 space-y-2">
|
||||||
|
<nuxt-link v-for="link in navLinks" :key="link.path" :to="link.path"
|
||||||
|
class="block px-3 py-3 rounded-lg text-base font-medium text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-blue-600"
|
||||||
|
active-class="bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"
|
||||||
|
@click="isMobileMenuOpen = false">
|
||||||
|
{{ link.name }}
|
||||||
|
</nuxt-link>
|
||||||
|
<template v-if="!authStore?.token">
|
||||||
|
<nuxt-link to="/auth/login"
|
||||||
|
class="block w-full text-center mt-6 px-5 py-3 text-base font-bold text-white bg-blue-600 rounded-lg shadow-lg shadow-blue-500/30">
|
||||||
|
Sign In
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="w-full min-h-screen">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const isLoggedIn = !!(authStore.user || authStore.token);
|
||||||
|
const mustChangePassword = authStore.user?.mustChangePassword === true;
|
||||||
|
|
||||||
|
const isAuthPage = to.path.startsWith('/auth');
|
||||||
|
const isUpdatePasswordPage = to.path === '/profile/update-password';
|
||||||
|
|
||||||
|
if (!isLoggedIn && !isAuthPage) {
|
||||||
|
return navigateTo('/auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn && mustChangePassword && !isUpdatePasswordPage) {
|
||||||
|
return navigateTo('/profile/update-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn && isAuthPage) {
|
||||||
|
return navigateTo('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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>
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<script setup>
|
||||||
|
const email = ref("");
|
||||||
|
const error = ref(null);
|
||||||
|
const message = ref("");
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const api = config.public.apiBase;
|
||||||
|
const router = useRouter();
|
||||||
|
const notifications = useNotificationStore();
|
||||||
|
|
||||||
|
async function handleResetRequest() {
|
||||||
|
error.value = null;
|
||||||
|
message.value = "";
|
||||||
|
|
||||||
|
if (!email.value) {
|
||||||
|
error.value = "Email is required.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\S+@\S+\.\S+$/.test(email.value)) {
|
||||||
|
error.value = "Please enter a valid email address.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch(`${api}/auth/recover-password`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { email: email.value },
|
||||||
|
onResponse({ response }) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
notifications.success("Password reset email sent.");
|
||||||
|
router.push("/auth/login")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResponseError() {
|
||||||
|
error.value = "Email not found in our records.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (!error.value) error.value = "Network error: Connection to server failed.";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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">Reset Password</h1>
|
||||||
|
|
||||||
|
<form class="space-y-6" @submit.prevent="handleResetRequest">
|
||||||
|
<div>
|
||||||
|
<label for="email"
|
||||||
|
class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<input id="email" v-model="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',
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-4 pt-2">
|
||||||
|
<button type="submit" :disabled="loading"
|
||||||
|
class="w-full 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 ? 'SENDING...' : 'SEND RESET LINK' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nuxt-link to="/auth/login"
|
||||||
|
class="text-sm text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 transition-colors">
|
||||||
|
Back to Login
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+39
-45
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<div
|
<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">
|
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>
|
<h1 class="text-3xl font-bold text-gray-800 dark:text-white! mb-8 text-center capitalize">Login</h1>
|
||||||
|
|
||||||
<div v-if="globalError"
|
<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">
|
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">
|
||||||
@@ -19,24 +19,29 @@
|
|||||||
<input v-model="loginFormData.email" type="email" :class="[
|
<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',
|
'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'
|
errors.email ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
||||||
]" placeholder="[email protected]" />
|
]" 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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">Password</label>
|
<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="[
|
<input v-model="loginFormData.password" type="password" :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',
|
'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'
|
errors.password ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
||||||
]" placeholder="••••••••" />
|
]" 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>
|
||||||
|
|
||||||
<div class="flex justify-center pt-4">
|
<div class="flex flex-col items-center gap-4 pt-2">
|
||||||
<button @click="handleLogin" :disabled="loading"
|
<button :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">
|
class="w-full 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"
|
||||||
|
@click="handleLogin">
|
||||||
{{ loading ? 'AUTHENTICATING...' : 'LOGIN' }}
|
{{ loading ? 'AUTHENTICATING...' : 'LOGIN' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<nuxt-link to="/auth/forgot-password" class="text-xs text-blue-600 hover:underline dark:text-blue-400">
|
||||||
|
Forgot Password?
|
||||||
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,28 +49,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<script setup>
|
||||||
import { useAuthStore } from "~/stores/auth-store.js";
|
import { useAuthStore } from "~/stores/auth-store.js";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
@@ -111,8 +94,11 @@ async function handleLogin() {
|
|||||||
console.log(response)
|
console.log(response)
|
||||||
token.value = response._data.token;
|
token.value = response._data.token;
|
||||||
user.value = response._data.user;
|
user.value = response._data.user;
|
||||||
router.push("/")
|
if (response._data.user.mustChangePassword === true) {
|
||||||
//getUserInfo();
|
router.push("/profile/update-password");
|
||||||
|
} else {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onResponseError({ response }) {
|
onResponseError({ response }) {
|
||||||
@@ -130,18 +116,26 @@ async function handleLogin() {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
async function getUserInfo() {
|
to {
|
||||||
try {
|
opacity: 1;
|
||||||
const data = await $fetch(`${api}/auth/user`, {
|
transform: translateY(0);
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token.value}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
user.value = data;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("User info fetch failed:", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fadeInUp 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-200 {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+7
-7
@@ -4,7 +4,7 @@
|
|||||||
<div class="w-full max-w-2xl animate-[fadeInUp_0.6s_ease-out]">
|
<div class="w-full max-w-2xl animate-[fadeInUp_0.6s_ease-out]">
|
||||||
|
|
||||||
<header class="text-center mb-12">
|
<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">
|
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 dark:text-white! mb-4 tracking-tight">
|
||||||
Academic Management System
|
Academic Management System
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-md mx-auto">
|
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-md mx-auto">
|
||||||
@@ -15,17 +15,17 @@
|
|||||||
<nav class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<nav class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|
||||||
<nuxt-link to="/auth/login"
|
<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">
|
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>
|
<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>
|
<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
|
<p class="text-sm text-gray-500 dark:text-gray-400">Sign in to manage your personalized dashboard and academic
|
||||||
data.</p>
|
data.</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/auth-test"
|
<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">
|
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>
|
<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>
|
<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
|
<p class="text-sm text-gray-500 dark:text-gray-400">Verify API connections and authentication state
|
||||||
persistence.</p>
|
persistence.</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
<div
|
<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">
|
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="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="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>
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
|
||||||
</span>
|
</span>
|
||||||
System Operational
|
System Operational
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+254
-2
@@ -1,5 +1,257 @@
|
|||||||
<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 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">
|
||||||
|
<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 dark:bg-slate-900 rounded-2xl p-6 shadow-sm border border-slate-200 dark:border-slate-800 transition-colors duration-300">
|
||||||
|
<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 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="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 dark:text-white tracking-tight">{{ user.name }}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="text-sm font-semibold text-slate-500 dark:text-slate-400 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 dark:bg-blue-900 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">
|
||||||
|
Edit Profile
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full py-2.5 px-4 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200 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 dark:border-slate-800 space-y-4">
|
||||||
|
<div class="flex items-center gap-3 text-sm text-slate-600 dark:text-slate-400">
|
||||||
|
<svg class="w-4 h-4 text-slate-400 dark:text-slate-500 shrink-0" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="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 dark:text-slate-400">
|
||||||
|
<svg class="w-4 h-4 text-slate-400 dark:text-slate-500 shrink-0" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="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 dark:text-slate-400">
|
||||||
|
<svg class="w-4 h-4 text-slate-400 dark:text-slate-500 shrink-0" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
<span>Joined {{ formattedDate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div
|
||||||
|
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">{{ totalCitations }}</div>
|
||||||
|
<div
|
||||||
|
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-1">
|
||||||
|
Citations
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
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">{{ user.publications.length }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-[10px] font-bold text-slate-400 dark:text-slate-500 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 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
|
||||||
|
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-semibold bg-slate-100 dark:bg-slate-800 text-slate-900 dark:text-white shadow-sm ring-1 ring-black/5 dark:ring-white/5 cursor-pointer transition-colors">
|
||||||
|
Overview
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex-1 min-w-[100px] py-2.5 px-4 rounded-lg text-lg font-medium text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-50 dark:hover:bg-slate-800 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 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors cursor-pointer">
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
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-s font-bold text-slate-900 dark:text-slate-100 uppercase tracking-wider mb-3">
|
||||||
|
Biography</h2>
|
||||||
|
<p class="text-slate-700 dark:text-slate-300 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 dark:text-slate-300 flex items-center gap-2">
|
||||||
|
Recent Publications
|
||||||
|
<span
|
||||||
|
class="bg-slate-200 dark:bg-slate-800 text-slate-700 dark:text-slate-300 text-xs font-bold px-2 py-0.5 rounded-full">{{
|
||||||
|
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 dark:border-slate-800 rounded-xl bg-slate-50/50 dark:bg-slate-900/50">
|
||||||
|
<p class="text-slate-500 dark:text-slate-400 font-medium">No publications found yet.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="pub in user.publications" :key="pub.id"
|
||||||
|
class="bg-white dark:bg-slate-900 p-6 rounded-xl shadow-sm border border-slate-200 dark:border-slate-800 hover:shadow-md hover:border-blue-300 dark:hover:border-blue-700 transition-all duration-200 group relative">
|
||||||
|
<div class="flex justify-between items-start gap-4 mb-2">
|
||||||
|
<h3
|
||||||
|
class="text-base font-bold text-slate-900 dark:text-slate-100 group-hover:text-blue-600 dark:group-hover:text-blue-400 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 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-xs font-bold border border-slate-200 dark:border-slate-700">
|
||||||
|
{{ pub.publishedYear }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-slate-600 dark:text-slate-400 text-sm leading-relaxed line-clamp-2">
|
||||||
|
{{ pub.abstract }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mt-4 pt-4 border-t border-slate-50 dark:border-slate-800 flex flex-wrap items-center gap-4 text-xs font-medium 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">
|
||||||
|
{{ pub.field }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center gap-1.5 text-slate-500 dark:text-slate-400">
|
||||||
|
<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 dark:text-slate-700">•</span>
|
||||||
|
<span class="text-slate-500 dark:text-slate-400">{{ pub.publishedDate }}</span>
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<script setup>
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const api = config.public.apiBase;
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const notifications = useNotificationStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const passwordData = reactive({
|
||||||
|
oldPassword: "",
|
||||||
|
newPassword: "",
|
||||||
|
confirmNewPassword: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
async function handleUpdatePassword() {
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
if (passwordData.newPassword !== passwordData.confirmNewPassword) {
|
||||||
|
error.value = "Passwords do not match.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch(`${api}/users/me/password`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authStore.token}`
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
oldPassword: passwordData.oldPassword,
|
||||||
|
newPassword: passwordData.newPassword,
|
||||||
|
confirmNewPassword: passwordData.confirmNewPassword
|
||||||
|
},
|
||||||
|
onResponse({ response }) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
notifications.success("Password updated successfully.");
|
||||||
|
authStore.user.mustChangePassword = false;
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error.value = "Failed to update password. Please check your current password";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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-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>
|
||||||
|
|
||||||
|
<form class="space-y-5" @submit.prevent="handleUpdatePassword">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">Current
|
||||||
|
Password</label>
|
||||||
|
<input v-model="passwordData.oldPassword" type="password" required
|
||||||
|
class="w-full px-4 py-2.5 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-gray-900 dark:text-white"
|
||||||
|
placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-800 my-2">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">New
|
||||||
|
Password</label>
|
||||||
|
<input v-model="passwordData.newPassword" type="password" required
|
||||||
|
class="w-full px-4 py-2.5 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-gray-900 dark:text-white"
|
||||||
|
placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">Confirm
|
||||||
|
New Password</label>
|
||||||
|
<input v-model="passwordData.confirmNewPassword" type="password" required
|
||||||
|
class="w-full px-4 py-2.5 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-gray-900 dark:text-white"
|
||||||
|
placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error"
|
||||||
|
class="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-xs text-center">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-4 pt-4">
|
||||||
|
<button type="submit" :disabled="loading"
|
||||||
|
class="w-full 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 capitalize">
|
||||||
|
{{ loading ? 'Updating...' : 'Update Password' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useNotificationStore = defineStore('notifications', {
|
||||||
|
state: () => ({
|
||||||
|
notifications: []
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
add(message, type = 'success', duration = 3000) {
|
||||||
|
const id = Date.now()
|
||||||
|
this.notifications.push({ id, message, type })
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.remove(id)
|
||||||
|
}, duration)
|
||||||
|
},
|
||||||
|
remove(id) {
|
||||||
|
this.notifications = this.notifications.filter(n => n.id !== id)
|
||||||
|
},
|
||||||
|
error(msg) { this.add(msg, 'error') },
|
||||||
|
success(msg) { this.add(msg, 'success') }
|
||||||
|
}
|
||||||
|
})
|
||||||
+32
-14
@@ -1,20 +1,38 @@
|
|||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2026-01-01",
|
compatibilityDate: "2026-01-01",
|
||||||
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"],
|
runtimeConfig: {
|
||||||
});
|
public: {
|
||||||
|
apiBase: "http://localhost:8080/academics/api/v1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
modules: [
|
||||||
|
"@nuxt/fonts",
|
||||||
|
"@nuxt/icon",
|
||||||
|
"@nuxt/image",
|
||||||
|
"@nuxt/eslint",
|
||||||
|
"shadcn-nuxt",
|
||||||
|
"@pinia/nuxt",
|
||||||
|
],
|
||||||
|
|
||||||
|
shadcn: {
|
||||||
|
prefix: "",
|
||||||
|
/**
|
||||||
|
* Directory that the component lives in.
|
||||||
|
* @default "./app/components/ui"
|
||||||
|
*/
|
||||||
|
componentDir: "./app/components/ui",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Generated
+68
-21
@@ -11,6 +11,7 @@
|
|||||||
"@nuxt/fonts": "^0.11.4",
|
"@nuxt/fonts": "^0.11.4",
|
||||||
"@nuxt/icon": "^2.1.0",
|
"@nuxt/icon": "^2.1.0",
|
||||||
"@nuxt/image": "^1.11.0",
|
"@nuxt/image": "^1.11.0",
|
||||||
|
"@pinia/nuxt": "0.11.3",
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@vueuse/core": "^14.0.0",
|
"@vueuse/core": "^14.0.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"eslint": "^9.39.0",
|
"eslint": "^9.39.0",
|
||||||
"lucide-vue-next": "^0.552.0",
|
"lucide-vue-next": "^0.552.0",
|
||||||
"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",
|
||||||
@@ -3439,6 +3441,21 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@pinia/nuxt": {
|
||||||
|
"version": "0.11.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.11.3.tgz",
|
||||||
|
"integrity": "sha512-7WVNHpWx4qAEzOlnyrRC88kYrwnlR/PrThWT0XI1dSNyUAXu/KBv9oR37uCgYkZroqP5jn8DfzbkNF3BtKvE9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@nuxt/kit": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"pinia": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -5106,12 +5123,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-kit": {
|
"node_modules/@vue/devtools-kit": {
|
||||||
"version": "7.7.7",
|
"version": "7.7.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
||||||
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
|
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-shared": "^7.7.7",
|
"@vue/devtools-shared": "^7.7.9",
|
||||||
"birpc": "^2.3.0",
|
"birpc": "^2.3.0",
|
||||||
"hookable": "^5.5.3",
|
"hookable": "^5.5.3",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
@@ -5127,9 +5144,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-shared": {
|
"node_modules/@vue/devtools-shared": {
|
||||||
"version": "7.7.7",
|
"version": "7.7.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
||||||
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
|
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"rfdc": "^1.4.1"
|
"rfdc": "^1.4.1"
|
||||||
@@ -7006,9 +7023,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/devalue": {
|
"node_modules/devalue": {
|
||||||
"version": "5.4.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||||
"integrity": "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==",
|
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/dfa": {
|
"node_modules/dfa": {
|
||||||
@@ -8409,9 +8426,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/h3": {
|
"node_modules/h3": {
|
||||||
"version": "1.15.4",
|
"version": "1.15.5",
|
||||||
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.4.tgz",
|
"resolved": "https://registry.npmjs.org/h3/-/h3-1.15.5.tgz",
|
||||||
"integrity": "sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==",
|
"integrity": "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie-es": "^1.2.2",
|
"cookie-es": "^1.2.2",
|
||||||
@@ -8419,9 +8436,9 @@
|
|||||||
"defu": "^6.1.4",
|
"defu": "^6.1.4",
|
||||||
"destr": "^2.0.5",
|
"destr": "^2.0.5",
|
||||||
"iron-webcrypto": "^1.2.1",
|
"iron-webcrypto": "^1.2.1",
|
||||||
"node-mock-http": "^1.0.2",
|
"node-mock-http": "^1.0.4",
|
||||||
"radix3": "^1.1.2",
|
"radix3": "^1.1.2",
|
||||||
"ufo": "^1.6.1",
|
"ufo": "^1.6.3",
|
||||||
"uncrypto": "^0.1.3"
|
"uncrypto": "^0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -10181,9 +10198,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-mock-http": {
|
"node_modules/node-mock-http": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz",
|
||||||
"integrity": "sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==",
|
"integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
@@ -10814,6 +10831,36 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pinia": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-api": "^7.7.7"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/posva"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.5.0",
|
||||||
|
"vue": "^3.5.11"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pinia/node_modules/@vue/devtools-api": {
|
||||||
|
"version": "7.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
|
||||||
|
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/devtools-kit": "^7.7.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pkg-types": {
|
"node_modules/pkg-types": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||||
@@ -13273,9 +13320,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ufo": {
|
"node_modules/ufo": {
|
||||||
"version": "1.6.1",
|
"version": "1.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
|
||||||
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
|
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/ultrahtml": {
|
"node_modules/ultrahtml": {
|
||||||
|
|||||||
Reference in New Issue
Block a user