Compare commits

...

15 Commits

Author SHA1 Message Date
90bf54e991 Update list display layout and responsiveness 2025-08-31 18:12:34 +02:00
6885df22e8 Add @vueuse/core dependency 2025-08-31 18:12:19 +02:00
84e1ca555c Add default avatar for users without profile image 2025-08-16 15:58:10 +02:00
0fc0aa95d3 Add error handling to login and improve user profile
The login changes add try/catch error handling with a user-friendly
message, while the profile page now displays first and last names
separately. Also includes a fix for optional chaining in index.vue.
2025-08-16 15:07:09 +02:00
d65fe6e353 Fix image upload validation and clearing 2025-08-16 12:56:58 +02:00
021bd8aae2 Enhance list items UI and progress display 2025-08-16 12:00:33 +02:00
82fb58c785 Add user email and improve nav title formatting 2025-08-16 12:00:18 +02:00
52cfe98a1b Fix file input clear and improve upload handling
The change adds image clearing functionality and improves input
validation by adding null checks and clearable state to the file input
component.
2025-08-15 11:32:42 +02:00
36f8bac508 Update NavBar styling and theme controls 2025-08-15 11:32:31 +02:00
a04a66a176 Add minimal and soft color themes 2025-08-15 11:32:21 +02:00
6dc8f5c39e Update list item color and background styles 2025-08-15 10:05:11 +02:00
8b55efa318 Use theme prop instead of class for v-main background 2025-08-15 10:04:59 +02:00
28beddb785 Add theme customization and component defaults 2025-08-15 10:04:45 +02:00
2abfd38dd7 Add list view component to homepage 2025-08-14 16:37:52 +02:00
1cdfafc90f Add padding to navigation drawer profile item 2025-08-14 16:37:42 +02:00
8 changed files with 302 additions and 45 deletions

View File

@@ -3,13 +3,7 @@
<NavBar />
<!-- Main content of the application -->
<v-main
:class="[
$vuetify.theme.current.dark
? 'bg-grey-darken-1'
: 'bg-green-lighten-5',
]"
>
<v-main :theme="$vuetify.theme.current.dark ? 'dark' : 'light'">
<NuxtPage />
</v-main>

View File

@@ -1,16 +1,27 @@
<template>
<!-- Toolbar at the top of the screen -->
<v-app-bar
color="rgba(27, 94, 32, 0.8)"
app
color="primary-darken-1"
class="text-white"
height="48"
v-if="status !== 'unauthenticated'"
>
<!-- Title of the application with adjusted margin and font size -->
<v-app-bar-title class="text-h6 ms-3 text-white">
<v-icon icon="mdi-apps" @click="drawer = !drawer"></v-icon>
<v-icon
color="info"
icon="mdi-apps"
@click="drawer = !drawer"
></v-icon>
<span class="ms-1 text-white"
>Listas de Front - {{ capitalize($route.name) }}</span
<span class="ms-1 text-info"
>{{ data?.email ? data?.email : "" }} -
{{
capitalize($route.name) == "Index"
? "Listas"
: capitalize($route.name)
}}</span
>
</v-app-bar-title>
@@ -20,19 +31,22 @@
<!-- Menu icons on the right side of the toolbar -->
<template v-slot:append>
<v-btn
class="text-orange"
color="info"
@click="theme.toggle()"
text="Light / Dark"
></v-btn>
<small class="text-white"> Logout </small>
<v-btn icon="mdi-account" class="text-white" @click="logout">
</v-btn>
<v-btn icon="mdi-logout" color="warning" @click="logout"> </v-btn>
</template>
</v-app-bar>
<v-navigation-drawer v-model="drawer" temporary>
<v-list-item
:prepend-avatar="data && data.image ? data.image : ''"
class="ma-4 ml-8"
:prepend-avatar="
data && data.image
? data.image
: 'https://cdn.vuetifyjs.com/images/logos/v-alt.svg'
"
:title="data && data.name ? data.name : ''"
></v-list-item>

View File

@@ -123,11 +123,16 @@ const passwordRules = [
"La contraseña debe tener al menos 5 caracteres",
];
// TODO - cuando hace login cambiar el último login del usuario
const login = async () => {
await signIn(
{ username: "admin", password: "admin" },
{ callbackUrl: "/" },
);
try {
await signIn(
{ username: "joseko", password: "kpdrlp00" },
{ callbackUrl: "/" },
);
} catch (error) {
alert("Ha ocurrido un error. Comprueba tus credenciales");
}
};
</script>

View File

@@ -1,12 +1,58 @@
<template>
<v-container
class="fill-height d-flex flex-column align-center justify-center text-center"
class="d-flex flex-column align-center justify-center text-center"
>
<div class="mt-3">My Application's Home Page</div>
<div>
{{ listasStore.listas }}
{{ data }}
</div>
<div class="mt-3">Listas</div>
<v-card
class="ma-5 mx-auto"
v-if="listasStore.listas.length > 0"
max-width="100%"
width="1000%"
>
<v-list-item
class="text-black ma-10 pa-5 rounded-lg full-height flex-column"
:class="
lista.owner == data?.email
? 'bg-green-lighten-2'
: 'bg-green-darken-1'
"
v-for="lista in listasStore.listas"
:key="lista.id"
:subtitle="lista.descripcion"
:title="lista.name"
>
<template v-slot:prepend v-if="width > 600">
<div class="d-flex flex-column full-height">
<small class="ml-2 text-red">{{ lista.owner }}</small>
<template v-if="lista.sharedListas.length > 0">
<small
v-for="shared in lista.sharedListas"
:key="shared"
class="ml-2 text-orange-darken-3"
>{{ shared }}</small
></template
>
</div>
</template>
<!-- TODO template número de compartidos -->
<template v-slot:append>
<v-badge
class="mr-3"
location="top right"
color="grey-lighten-2"
:content="`${lista.countItems}/${lista.completedItems}`"
>
<v-btn
color="black"
icon="mdi-counter"
variant="text"
></v-btn>
</v-badge>
</template> </v-list-item
></v-card>
<div></div>
</v-container>
</template>
@@ -14,6 +60,7 @@
definePageMeta({
auth: true,
});
import { useWindowSize } from "@vueuse/core";
const { data } = useAuth();
import { ref, onMounted } from "vue";
import { useSystemStore } from "~/stores/system";
@@ -21,8 +68,10 @@ import { useListasStore } from "~/stores/listas";
const listasStore = useListasStore();
const systemStore = useSystemStore();
const { width, height } = useWindowSize();
onMounted(async () => {
await listasStore.getData();
console.log(listasStore.listas);
});
</script>

View File

@@ -20,8 +20,10 @@
@input="handleFileInput"
type="file"
v-model="imagen"
click:clear="limpiar_input"
clearable
/><v-btn
color="primary"
color="secundary"
:disabled="files.length <= 0 || !validateFileType()"
@click="uploadImage"
>
@@ -34,7 +36,9 @@
<h3>Username:</h3>
<p>{{ data.username }}</p>
<h3>Nombre:</h3>
<p>{{ data.name }} {{ data.last_name }}</p>
<p>{{ data.name }}</p>
<h3>Apellidos:</h3>
<p>{{ data.last_name }}</p>
<h3>Correo:</h3>
<p>{{ data.email }}</p>
<h3>Listas:</h3>
@@ -81,7 +85,9 @@ const uploadImage = async () => {
if (response) {
//imagenVista.value = response.image;
files.value.pop();
imagen.value = null;
if (imagen.value != null) {
imagen.value = null;
}
await getSession();
} else {
console.error(response);
@@ -89,21 +95,23 @@ const uploadImage = async () => {
};
const validateFileType = () => {
var fileName = imagen.value.name;
var idxDot = fileName.lastIndexOf(".") + 1;
var extFile = fileName.substr(idxDot, fileName.length).toLowerCase();
if (
extFile == "jpg" ||
extFile == "jpeg" ||
extFile == "png" ||
extFile == "gif"
) {
return true;
} else {
alert("Only jpg, jpeg, png and gif files are allowed!");
files.value.pop();
imagen.value = null;
return false;
if (imagen.value != null) {
var fileName = imagen.value.name;
var idxDot = fileName.lastIndexOf(".") + 1;
var extFile = fileName.substr(idxDot, fileName.length).toLowerCase();
if (
extFile == "jpg" ||
extFile == "jpeg" ||
extFile == "png" ||
extFile == "gif"
) {
return true;
} else {
alert("Only jpg, jpeg, png and gif files are allowed!");
files.value.pop();
imagen.value = null;
return false;
}
}
};
@@ -115,6 +123,11 @@ const openImageDialog = () => {
// TODO Implement image dialog logic
};
const limpiar_input = () => {
imagen.value = null;
files.value.pop();
};
onMounted(async () => {
await listasStore.getData();
});

View File

@@ -1,11 +1,147 @@
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
const lightTheme = {
dark: false,
colors: {
background: "#F8FAF7",
surface: "#FFFFFF",
primary: "#88D8B0",
"primary-darken-1": "#6BC49A",
secondary: "#C8E6C9",
"secondary-darken-1": "#A5D6A7",
accent: "#D4EDDA",
error: "#FF9A8B",
info: "#B3E0FF",
success: "#A5D6A7",
warning: "#FFE0B2",
"on-primary": "#2E3A32",
"on-surface": "#2E3A32",
},
variables: {
"border-color": "#E0E0E0",
"border-opacity": 0.12,
},
};
const darkTheme = {
dark: true,
colors: {
background: "#121F17",
surface: "#1E2922",
primary: "#4A7856",
"primary-darken-1": "#3A6A46",
secondary: "#3D5A45",
"secondary-darken-1": "#2D4A35",
accent: "#5D8C6C",
error: "#D32F2F",
info: "#2196F3",
success: "#4CAF50",
warning: "#FFA000",
"on-primary": "#E8F5E9",
"on-surface": "#E0E0E0",
},
variables: {
"border-color": "#37474F",
"border-opacity": 0.24,
},
};
const minimalLightTheme = {
dark: false,
colors: {
background: "#FFFFFF",
surface: "#F5F5F5",
primary: "#7C90DB", // Azul suave
"primary-darken-1": "#6A7EC9",
secondary: "#E0E0E0", // Gris neutro
"secondary-darken-1": "#C7C7C7",
accent: "#A5C4D4", // Azul pastel
error: "#FF6B6B", // Rojo coral suave
info: "#88C9F2",
success: "#77DD77", // Verde menta
warning: "#FFD166", // Amarillo pastel
"on-primary": "#2D3748", // Gris oscuro
"on-surface": "#2D3748",
},
variables: {
"border-color": "#E2E8F0",
"border-opacity": 0.16,
},
};
const minimalDarkTheme = {
dark: true,
colors: {
background: "#1E2B3A",
surface: "#2C3E50",
primary: "#8A9EFF", // Azul lavanda (inalterado)
"primary-darken-1": "#778CEB",
secondary: "#4A5568", // Gris azulado (reemplaza grises neutros)
"secondary-darken-1": "#3C4758",
accent: "#A7BED3", // Azul polvo (inalterado)
error: "#FF8A8A",
info: "#90CAF9",
success: "#81C784",
warning: "#FFD54F",
"on-primary": "#F7FAFC", // Blanco casi puro (mejor contraste)
"on-surface": "#E2E8F0", // Gris claro azulado
},
variables: {
"border-color": "#4A5568", // Coordinado con secondary
"border-opacity": 0.3,
},
};
const softDarkTheme = {
dark: true,
colors: {
background: "#2C3E50", // Gris-azul medio (como cielo nocturno)
surface: "#34495E", // 15% más claro que el fondo
primary: "#7F9CF5", // Azul lavanda más claro
"primary-darken-1": "#6B8BEA",
secondary: "#4B5563", // Gris neutro cálido
"secondary-darken-1": "#3E4A5A",
accent: "#94A3B8", // Gris-azul claro (para detalles)
error: "#FCA5A5", // Rojo suave
info: "#93C5FD", // Azul cielo
success: "#86EFAC", // Verde menta claro
warning: "#FCD34D", // Amarillo miel
"on-primary": "#F8FAFC", // Blanco niebla
"on-surface": "#E5E7EB", // Gris muy claro
},
variables: {
"border-color": "#475569", // Borde sutil
"border-opacity": 0.25,
},
};
export default defineNuxtPlugin((nuxtApp) => {
const vuetify = createVuetify({
components,
directives,
theme: {
defaultTheme: "light", // 'light' | 'dark' | 'system'
defaultTheme: "light",
themes: {
light: minimalLightTheme,
dark: softDarkTheme,
},
variations: {
colors: ["primary", "secondary"],
lighten: 2,
darken: 2,
},
},
defaults: {
VBtn: {
color: "primary",
variant: "flat",
},
VCard: {
elevation: 1,
rounded: "lg",
},
},
});

45
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"@mdi/font": "^7.4.47",
"@pinia/nuxt": "^0.11.2",
"@sidebase/nuxt-auth": "^1.0.1",
"@vueuse/core": "^13.8.0",
"nuxt": "^4.0.1",
"pinia": "^3.0.3",
"vue": "^3.5.18",
@@ -3874,6 +3875,12 @@
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
"license": "MIT"
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@@ -4298,6 +4305,44 @@
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
"license": "MIT"
},
"node_modules/@vueuse/core": {
"version": "13.8.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.8.0.tgz",
"integrity": "sha512-rmBcgpEpxY0ZmyQQR94q1qkUcHREiLxQwNyWrtjMDipD0WTH/JBcAt0gdcn2PsH0SA76ec291cHFngmyaBhlxA==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.21",
"@vueuse/metadata": "13.8.0",
"@vueuse/shared": "13.8.0"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@vueuse/metadata": {
"version": "13.8.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.8.0.tgz",
"integrity": "sha512-BYMp3Gp1kBUPv7AfQnJYP96mkX7g7cKdTIgwv/Jgd+pfQhz678naoZOAcknRtPLP4cFblDDW7rF4e3KFa+PfIA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "13.8.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.8.0.tgz",
"integrity": "sha512-x4nfM0ykW+RmNJ4/1IzZsuLuWWrNTxlTWUiehTGI54wnOxIgI9EDdu/O5S77ac6hvQ3hk2KpOVFHaM0M796Kbw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/@whatwg-node/disposablestack": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",

View File

@@ -13,6 +13,7 @@
"@mdi/font": "^7.4.47",
"@pinia/nuxt": "^0.11.2",
"@sidebase/nuxt-auth": "^1.0.1",
"@vueuse/core": "^13.8.0",
"nuxt": "^4.0.1",
"pinia": "^3.0.3",
"vue": "^3.5.18",