feat: improve catalog responsive design, cart layout and card component
- Redesign Cart component with improved alignments, gaps, and responsive padding - Refactor Card component with mobile-first layout and consistent spacing - Update catalog layout to 60/40 split for products/cart on desktop (lg) - Add PaginationControls component (previously untracked) - Clean up obsolete CSS styles from catalog page
This commit is contained in:
@@ -1,78 +1,142 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-card :class="{
|
||||
'cart-mobile-expanded': isMobile && !isCollapsed,
|
||||
'cart-collapsed-mobile': isMobile && isCollapsed,
|
||||
'cart-desktop-collapsed': !isMobile && isCollapsed
|
||||
}">
|
||||
<v-card-title
|
||||
class="d-flex align-center cart-title"
|
||||
:class="{ 'cart-header-mobile': isMobile, 'cart-header-desktop': !isMobile }"
|
||||
>
|
||||
<!-- Icono del carrito - SIEMPRE VISIBLE -->
|
||||
<v-icon class="mr-2">mdi-cart</v-icon>
|
||||
Carrito
|
||||
<v-chip v-if="cartCount" color="primary" class="ml-2">{{ cartCount }}</v-chip>
|
||||
|
||||
<!-- Texto "Carrito" - SIEMPRE VISIBLE -->
|
||||
<span class="cart-title-text">Carrito</span>
|
||||
|
||||
<!-- Cantidad de productos - SIEMPRE VISIBLE -->
|
||||
<v-chip
|
||||
v-if="cartCount > 0"
|
||||
color="primary"
|
||||
class="ml-2"
|
||||
size="small"
|
||||
>
|
||||
{{ cartCount }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
color="grey"
|
||||
class="ml-2"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
0
|
||||
</v-chip>
|
||||
|
||||
<!-- Total visible cuando está colapsado (mobile o desktop) -->
|
||||
<span
|
||||
v-if="isCollapsed && cartItems.length > 0"
|
||||
class="ml-auto text-subtitle-1 font-weight-bold mr-2"
|
||||
>
|
||||
{{ currency(cartTotal) }}
|
||||
</span>
|
||||
<v-spacer v-else></v-spacer>
|
||||
|
||||
<!-- Botón toggle SIEMPRE visible (mobile y desktop) -->
|
||||
<v-btn
|
||||
icon
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="$emit('toggle-collapse')"
|
||||
:title="isCollapsed ? 'Expandir carrito' : 'Contraer carrito'"
|
||||
>
|
||||
<v-icon>{{ isCollapsed ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text v-if="cartItems.length === 0" class="text-center grey--text">
|
||||
El carrito está vacío
|
||||
</v-card-text>
|
||||
|
||||
<v-list v-else density="compact" max-height="300" class="overflow-y-auto">
|
||||
<v-list-item v-for="item in cartItems" :key="item.id">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="40" rounded>
|
||||
<v-img :src="item.img" cover></v-img>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="d-flex align-center">
|
||||
<div class="quantity-controls">
|
||||
<v-btn small text class="qty-btn" @click="decreaseQuantity(item.id)"
|
||||
><v-icon small>mdi-minus</v-icon></v-btn
|
||||
>
|
||||
<v-text-field
|
||||
:model-value="item.quantity"
|
||||
@update:model-value="updateQuantity(item.id, $event)"
|
||||
type="number"
|
||||
min="1"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="qty-input"
|
||||
/>
|
||||
<v-btn small text class="qty-btn" @click="increaseQuantity(item.id)"
|
||||
><v-icon small>mdi-plus</v-icon></v-btn
|
||||
>
|
||||
<div v-show="!isCollapsed">
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text v-if="cartItems.length === 0" class="text-center grey--text">
|
||||
El carrito está vacío
|
||||
</v-card-text>
|
||||
|
||||
<v-list v-else density="compact" :max-height="listMaxHeight" class="overflow-y-auto cart-list">
|
||||
<v-list-item v-for="(item, index) in cartItems" :key="item.id" class="cart-list-item">
|
||||
<template v-slot:prepend>
|
||||
<div class="prepend-wrapper">
|
||||
<!-- Mostrar imagen solo si NO es extra small -->
|
||||
<v-avatar v-if="!isExtraSmall" size="40" rounded>
|
||||
<v-img :src="item.img" cover></v-img>
|
||||
</v-avatar>
|
||||
<!-- Mostrar número cuando es extra small -->
|
||||
<div v-else class="item-number">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Contenido reorganizado -->
|
||||
<div class="cart-item-content">
|
||||
<!-- Línea 1: Nombre del producto -->
|
||||
<div class="product-name">{{ item.name }}</div>
|
||||
|
||||
<!-- Línea 2: Controles + Precio unitario -->
|
||||
<div class="controls-row">
|
||||
<div class="quantity-controls">
|
||||
<v-btn small text class="qty-btn" @click="decreaseQuantity(item.id)">
|
||||
<v-icon small>mdi-minus</v-icon>
|
||||
</v-btn>
|
||||
<v-text-field
|
||||
:model-value="item.quantity"
|
||||
@update:model-value="updateQuantity(item.id, $event)"
|
||||
type="number"
|
||||
min="1"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="qty-input"
|
||||
/>
|
||||
<v-btn small text class="qty-btn" @click="increaseQuantity(item.id)">
|
||||
<v-icon small>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<span class="unit-price">x {{ currency(item.price) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="ml-2">x {{ currency(item.price) }}</span>
|
||||
</v-list-item-subtitle>
|
||||
|
||||
<template v-slot:append>
|
||||
<div class="d-flex align-center">
|
||||
<strong>{{ currency(item.price * item.quantity) }}</strong>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="red"
|
||||
class="ml-2"
|
||||
@click="$emit('remove', item.id)"
|
||||
></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider v-if="cartItems.length > 0"></v-divider>
|
||||
|
||||
<v-card-text v-if="cartItems.length > 0">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<strong>Total:</strong>
|
||||
<strong class="text-h6">{{ currency(cartTotal) }}</strong>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="cartItems.length > 0">
|
||||
<v-btn color="primary" block @click="$emit('checkout')">
|
||||
Finalizar Compra
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
|
||||
<!-- Append: Total + Delete -->
|
||||
<template v-slot:append>
|
||||
<div class="item-actions">
|
||||
<strong class="total-price">{{ currency(item.price * item.quantity) }}</strong>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
class="delete-btn"
|
||||
@click="$emit('remove', item.id)"
|
||||
></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider v-if="cartItems.length > 0"></v-divider>
|
||||
|
||||
<v-card-text v-if="cartItems.length > 0" class="cart-total-section">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<strong>Total:</strong>
|
||||
<strong class="text-h6">{{ currency(cartTotal) }}</strong>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions v-if="cartItems.length > 0" class="cart-checkout-section">
|
||||
<v-btn color="primary" block @click="$emit('checkout')">
|
||||
Finalizar Compra
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -87,15 +151,39 @@ export default {
|
||||
currency: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
windowWidth: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
emits: ['remove', 'checkout', 'update-quantity'],
|
||||
emits: ['remove', 'checkout', 'update-quantity', 'toggle-collapse'],
|
||||
computed: {
|
||||
cartCount() {
|
||||
return this.cartItems.reduce((sum, item) => sum + item.quantity, 0);
|
||||
},
|
||||
cartTotal() {
|
||||
return this.cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
},
|
||||
listMaxHeight() {
|
||||
// En desktop, permitir más altura para scroll
|
||||
if (!this.isMobile) {
|
||||
return '500px';
|
||||
}
|
||||
// En mobile, altura limitada
|
||||
return '300px';
|
||||
},
|
||||
isExtraSmall() {
|
||||
// Detectar si la resolución es menor a 560px
|
||||
return this.windowWidth < 560;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -122,20 +210,393 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.qty-input {
|
||||
width: 70px;
|
||||
/* === ESTILOS BASE PARA CART ITEMS (Mobile First) === */
|
||||
|
||||
/* Contenedor principal del item */
|
||||
.cart-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0; /* Permite que el flex funcione con overflow */
|
||||
}
|
||||
.qty-input input {
|
||||
text-align: center;
|
||||
|
||||
/* Nombre del producto */
|
||||
.product-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.3;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2; /* Máximo 2 líneas */
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Fila de controles + precio unitario */
|
||||
.controls-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Controles de cantidad */
|
||||
.quantity-controls {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Input de cantidad */
|
||||
.qty-input {
|
||||
width: 55px;
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.qty-input input {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Botones de cantidad */
|
||||
.qty-btn {
|
||||
min-width: 28px !important;
|
||||
height: 28px !important;
|
||||
border-radius: 14px !important;
|
||||
min-width: 24px !important;
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
border-radius: 12px !important;
|
||||
padding: 0 !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
.qty-btn .v-icon {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* Precio unitario */
|
||||
.unit-price {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Contenedor de acciones (total + delete) */
|
||||
.item-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Precio total */
|
||||
.total-price {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: #2e7d32;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Botón de eliminar */
|
||||
.delete-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Footer: Total y checkout */
|
||||
.cart-total-section {
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.cart-checkout-section {
|
||||
padding: 4px 16px 12px !important;
|
||||
}
|
||||
|
||||
/* Ajustes al list-item */
|
||||
.cart-list-item {
|
||||
padding: 10px 8px !important;
|
||||
min-height: auto !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* Wrapper del prepend para controlar gap con content */
|
||||
.prepend-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Estilos para mobile expandido */
|
||||
.cart-mobile-expanded {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px 16px 0 0 !important;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
/* Estilos para mobile colapsado */
|
||||
.cart-collapsed-mobile {
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
border-radius: 16px 16px 0 0 !important;
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Estilos para desktop colapsado */
|
||||
.cart-desktop-collapsed {
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Header del carrito */
|
||||
.cart-title {
|
||||
padding: 8px 12px !important;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.cart-title-text {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Header interactivo en mobile */
|
||||
.cart-header-mobile {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Header interactivo en desktop */
|
||||
.cart-header-desktop {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.cart-header-desktop:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Lista de items con scroll */
|
||||
.cart-list {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
||||
}
|
||||
|
||||
.cart-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.cart-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cart-list::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cart-list::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Transiciones suaves */
|
||||
.v-card {
|
||||
transition: max-height 0.35s ease-in-out, box-shadow 0.3s ease-in-out, border-radius 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* === MEDIA QUERIES RESPONSIVE === */
|
||||
|
||||
/* Resolución 560-959px (Mobile/Tablet con imagen) */
|
||||
@media (min-width: 560px) and (max-width: 959px) {
|
||||
.cart-title {
|
||||
padding: 10px 16px !important;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.cart-title-text {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.cart-list-item {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.prepend-wrapper {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.cart-item-content {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.qty-input {
|
||||
width: 65px !important;
|
||||
min-width: 65px !important;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
min-width: 28px !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
border-radius: 14px !important;
|
||||
}
|
||||
|
||||
.qty-btn .v-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.unit-price {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.cart-total-section {
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.cart-checkout-section {
|
||||
padding: 4px 16px 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resolución ≥960px (Desktop) */
|
||||
@media (min-width: 960px) {
|
||||
.v-card {
|
||||
border-radius: 12px !important;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.cart-title {
|
||||
padding: 12px 16px !important;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.cart-title-text {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.cart-list-item {
|
||||
padding: 12px 16px !important;
|
||||
}
|
||||
|
||||
.prepend-wrapper {
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.cart-item-content {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.controls-row {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.qty-input {
|
||||
width: 70px !important;
|
||||
min-width: 70px !important;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
min-width: 32px !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
border-radius: 16px !important;
|
||||
}
|
||||
|
||||
.qty-btn .v-icon {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.unit-price {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Item-actions en fila en desktop */
|
||||
.item-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cart-desktop-collapsed {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cart-total-section {
|
||||
padding: 14px 16px !important;
|
||||
}
|
||||
|
||||
.cart-checkout-section {
|
||||
padding: 4px 16px 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Ajustes para la lista de items en mobile */
|
||||
@media (max-width: 680px) {
|
||||
.v-list {
|
||||
max-height: calc(70vh - 200px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ajustes para resoluciones extra pequeñas (<560px) */
|
||||
@media (max-width: 559px) {
|
||||
/* Número del item cuando no hay imagen */
|
||||
.item-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
color: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.quantity-controls {
|
||||
gap: 3px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user