merge: merge conflics

This commit is contained in:
AfonsoCMSousa
2026-01-16 18:11:22 +00:00
10 changed files with 470 additions and 34 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
<template> <template>
<div> <NuxtLayout>
<NuxtPage /> <NuxtPage />
</div> </NuxtLayout>
</template> </template>
+1 -1
View File
@@ -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);
+68
View File
@@ -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>
+84
View File
@@ -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>
+147
View File
@@ -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>
+45 -3
View File
@@ -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>
+42
View File
@@ -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 };
});
+10 -6
View File
@@ -1,11 +1,11 @@
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", <<<<<<< HEAD
compatibilityDate: "2026-01-01",
devtools: { enabled: true }, devtools: { enabled: true },
css: [ css: [
"~/assets/css/main.css", "~/assets/css/main.css",
"~/assets/css/tailwind.css" "~/assets/css/tailwind.css"
], ],
@@ -13,18 +13,22 @@ export default defineNuxtConfig({
plugins: [tailwindcss()], plugins: [tailwindcss()],
}, },
runtimeConfig: {
public: {
apiBase: "http://localhost:8080/academics/api/v1",
},
},
modules: [ modules: [
"@nuxt/fonts", "@nuxt/fonts",
"@nuxt/icon", "@nuxt/icon",
"@nuxt/image", "@nuxt/image",
"@nuxt/eslint", "@nuxt/eslint",
"shadcn-nuxt", "shadcn-nuxt",
"@pinia/nuxt",
], ],
shadcn: { shadcn: {
/**
* Prefix for all the imported component
*/
prefix: "", prefix: "",
/** /**
* Directory that the component lives in. * Directory that the component lives in.
+68 -21
View File
@@ -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": {
+2
View File
@@ -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",