Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd0fc20b7d |
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<NuxtLayout>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</div>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<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,147 @@
|
|||||||
|
<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>
|
||||||
+44
-2
@@ -1,5 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div
|
||||||
<h1> This will be the home page </h1>
|
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]">
|
||||||
|
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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 };
|
||||||
|
});
|
||||||
+8
-20
@@ -1,32 +1,20 @@
|
|||||||
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: "2025-07-15",
|
compatibilityDate: "2026-01-01",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
css: ["~/assets/css/tailwind.css"],
|
css: ["~/assets/css/tailwind.css"],
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: [
|
runtimeConfig: {
|
||||||
"@nuxt/fonts",
|
public: {
|
||||||
"@nuxt/icon",
|
apiBase: "http://localhost:8080/academics/api/v1",
|
||||||
"@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",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
modules: ["@pinia/nuxt"],
|
||||||
});
|
});
|
||||||
@@ -20,7 +20,9 @@
|
|||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user