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:
2026-05-15 13:10:54 -05:00
parent 87f12f76ea
commit 99d3881d61
4 changed files with 1442 additions and 205 deletions

View File

@@ -1,23 +1,88 @@
<template>
<v-container fluid>
<!-- Backdrop para mobile cuando el carrito está expandido -->
<div
v-if="isMobile && !cartCollapsed"
class="cart-backdrop"
@click="cartCollapsed = true"
></div>
<v-row>
<v-col cols="12" md="9">
<v-col cols="12" md="9" lg="7" :class="{ 'pb-mobile-cart': isMobile }">
<v-card-title>
<span class="headline">Catálogo</span>
</v-card-title>
<v-list-item v-for="item in items" :key="item.id" class="catalog-item">
<Card :product="item" :increase="increase" :decrease="decrease" :currency="currency" :updateQuantity="updateQuantity" @add-to-cart="addToCart"/>
</v-list-item>
</v-col>
<v-col cols="12" md="3">
<div class="cart-sidebar">
<Cart
:cart-items="cartItems"
<!-- Controles de paginación superiores -->
<PaginationControls
v-if="items.length > 0"
:current-page="currentPage"
:total-pages="totalPages"
:items-per-page="itemsPerPage"
:items-per-page-options="itemsPerPageOptions"
:pagination-info="paginationInfo"
:total-visible-pages="totalVisiblePages"
@page-change="handlePageChange"
@items-per-page-change="handleItemsPerPageChange"
position="top"
/>
<!-- Lista de productos paginados -->
<v-list-item
v-for="item in paginatedItems"
:key="item.id"
class="catalog-item"
>
<Card
:product="item"
:increase="increase"
:decrease="decrease"
:currency="currency"
:updateQuantity="updateQuantity"
@add-to-cart="addToCart"
/>
</v-list-item>
<!-- Mensaje cuando no hay productos -->
<v-alert
v-if="items.length === 0"
type="info"
class="my-4"
variant="tonal"
>
No hay productos disponibles en el catálogo
</v-alert>
<!-- Controles de paginación inferiores -->
<PaginationControls
v-if="items.length > 0"
:current-page="currentPage"
:total-pages="totalPages"
:items-per-page="itemsPerPage"
:items-per-page-options="itemsPerPageOptions"
:pagination-info="paginationInfo"
:total-visible-pages="totalVisiblePages"
@page-change="handlePageChange"
@items-per-page-change="handleItemsPerPageChange"
position="bottom"
/>
</v-col>
<v-col cols="12" md="3" lg="5">
<div
class="cart-sidebar"
:class="{ collapsed: cartCollapsed && isMobile }"
>
<Cart
:cart-items="cartItems"
:currency="currency"
:is-collapsed="cartCollapsed"
:is-mobile="isMobile"
:window-width="windowWidth"
@remove="removeFromCart"
@checkout="goToCheckout"
@update-quantity="updateCartQuantity"
@toggle-collapse="toggleCart"
/>
</div>
</v-col>
@@ -26,24 +91,52 @@
</template>
<script>
import Card from '@/components/catalog/Card.vue';
import Cart from '@/components/catalog/Cart.vue';
import { useCartStore } from '@/stores/cart';
import { inject } from 'vue';
import Card from "@/components/catalog/Card.vue";
import Cart from "@/components/catalog/Cart.vue";
import PaginationControls from "@/components/catalog/PaginationControls.vue";
import { useCartStore } from "@/stores/cart";
import { inject, ref, computed, onMounted, onUnmounted } from "vue";
export default {
components: {
Card,
Cart
Cart,
PaginationControls,
},
setup() {
const cartStore = useCartStore();
return { cartStore };
const cartCollapsed = ref(false); // Cambiado a false para que inicie expandido en desktop
const windowWidth = ref(window.innerWidth);
const isMobile = computed(() => windowWidth.value < 960); // Cambiado de 680 a 960
const updateWindowWidth = () => {
windowWidth.value = window.innerWidth;
};
onMounted(() => {
window.addEventListener("resize", updateWindowWidth);
});
onUnmounted(() => {
window.removeEventListener("resize", updateWindowWidth);
});
return {
cartStore,
cartCollapsed,
isMobile,
windowWidth,
};
},
data() {
return {
api: inject('api'),
api: inject("api"),
items: [],
// Paginación
currentPage: 1,
itemsPerPage: 20,
itemsPerPageOptions: [10, 20, 50, 100],
};
},
computed: {
@@ -53,26 +146,74 @@ export default {
},
set(value) {
this.cartStore.items = value;
}
},
},
cartCount() {
return this.cartStore.cartCount;
}
},
// Paginación
paginatedItems() {
const start = (this.currentPage - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
return this.items.slice(start, end);
},
totalPages() {
return Math.ceil(this.items.length / this.itemsPerPage);
},
paginationInfo() {
const start = (this.currentPage - 1) * this.itemsPerPage + 1;
const end = Math.min(
this.currentPage * this.itemsPerPage,
this.items.length,
);
return {
start,
end,
total: this.items.length,
};
},
// Computed para total-visible dinámico y responsive (usado por ambos PaginationControls)
totalVisiblePages() {
// Si hay pocas páginas, mostrarlas todas (IMPORTANTE para mostrar iconos de navegación)
if (this.totalPages <= 7) {
return this.totalPages;
}
// Breakpoints responsivos basados en windowWidth
// OPTIMIZADO: Reducidos para evitar que desaparezcan los iconos de navegación
const width = this.windowWidth;
if (width < 400) {
return 3; // Extra small mobile
} else if (width < 680) {
return 5; // Mobile
} else if (width < 960) {
return 5; // Tablet (REDUCIDO de 7 → 5 para evitar overflow)
} else if (width < 1280) {
return 7; // Desktop small (REDUCIDO de 9 → 7)
} else {
return 9; // Desktop large (REDUCIDO de 11 → 9)
}
},
},
created() {
this.loadItemsPerPagePreference();
this.fetchProducts();
},
methods: {
fetchProducts() {
this.api.getProducts()
.then(data => {
this.items = data.map(product => ({
this.api
.getProducts()
.then((data) => {
this.items = data.map((product) => ({
...product,
quantity: 0,
img: product.img || `https://picsum.photos/300/200?random=${product.id}`
img:
product.img ||
`https://picsum.photos/300/200?random=${product.id}`,
}));
})
.catch(error => {
.catch((error) => {
console.error(error);
});
},
@@ -101,24 +242,62 @@ export default {
},
removeFromCart(itemId) {
this.cartStore.removeItem(itemId);
const item = this.items.find(i => i.id === itemId);
const item = this.items.find((i) => i.id === itemId);
if (item) {
item.quantity = 0;
}
},
updateCartQuantity({ itemId, quantity }) {
this.cartStore.updateQuantity({ itemId, quantity });
const productItem = this.items.find(i => i.id === itemId);
const productItem = this.items.find((i) => i.id === itemId);
if (productItem) {
productItem.quantity = quantity;
}
},
goToCheckout() {
this.$router.push('/comprar');
this.$router.push("/comprar");
},
toggleCart() {
this.cartCollapsed = !this.cartCollapsed;
},
// Paginación
handlePageChange(newPage) {
this.currentPage = newPage;
this.scrollToTop();
},
handleItemsPerPageChange(newValue) {
this.itemsPerPage = newValue;
this.currentPage = 1;
this.saveItemsPerPagePreference(newValue);
this.scrollToTop();
},
saveItemsPerPagePreference(value) {
localStorage.setItem("catalog_items_per_page", value);
},
loadItemsPerPagePreference() {
const saved = localStorage.getItem("catalog_items_per_page");
if (saved && this.itemsPerPageOptions.includes(parseInt(saved))) {
this.itemsPerPage = parseInt(saved);
}
},
scrollToTop() {
this.$nextTick(() => {
const catalogHeader = this.$el?.querySelector(".headline");
if (catalogHeader) {
catalogHeader.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
});
},
currency(val) {
if (val == null) return '-';
return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', minimumFractionDigits: 0 }).format(val);
if (val == null) return "-";
return new Intl.NumberFormat("es-CO", {
style: "currency",
currency: "COP",
minimumFractionDigits: 0,
}).format(val);
},
},
};
@@ -127,55 +306,94 @@ export default {
<style scoped>
.headline {
font-weight: bold;
font-size: 1.25rem;
}
/* === LISTADO DE PRODUCTOS === */
.catalog-item {
padding-top: 12px;
padding-bottom: 12px;
padding: 0;
margin-bottom: 12px;
}
.catalog-item:last-child {
margin-bottom: 0;
}
/* Mobile First: Estilos base (< 960px) */
.cart-sidebar {
position: sticky;
top: 80px;
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
transition: all 0.3s ease-in-out;
}
.product-img {
width: 150px;
height: 100px;
border-radius: 6px;
object-fit: cover;
.cart-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
animation: fadeIn 0.3s ease-in-out;
}
.quantity-controls {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.prices .price-line {
margin-bottom: 4px;
}
.quantity-input {
max-width: 90px;
}
.quantity-input input {
text-align: center;
}
.qty-btn {
min-width: 36px !important;
height: 36px !important;
border-radius: 18px !important;
}
@media (max-width: 960px) {
.cart-sidebar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
top: auto;
z-index: 100;
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (max-width: 600px) {
.product-img {
width: 120px;
height: 80px;
/* Padding para el contenido del catálogo en mobile */
.pb-mobile-cart {
padding-bottom: 76px !important; /* 60px sidebar + 16px margen */
}
/* Desktop (≥ 960px) - Sidebar sticky con scroll */
/* Layout: 960-1023px = md breakpoint (75% productos / 25% cart) */
/* Layout: ≥1024px = lg breakpoint (60% productos / 40% cart) */
@media (min-width: 960px) {
.cart-sidebar {
position: sticky;
top: 80px;
z-index: auto;
max-height: calc(100vh - 100px);
overflow-y: visible;
}
.cart-backdrop {
display: none;
}
.pb-mobile-cart {
padding-bottom: 16px !important;
}
.catalog-item {
margin-bottom: 16px;
}
}
/* Resolución <560px - Mobile extra small */
@media (max-width: 559px) {
.headline {
font-size: 1.1rem;
}
.catalog-item {
margin-bottom: 10px;
}
}
/* Resolución 560-959px - Tablet */
@media (min-width: 560px) and (max-width: 959px) {
.catalog-item {
margin-bottom: 14px;
}
}
</style>