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,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>
|
||||
|
||||
Reference in New Issue
Block a user