PDF PEDIDOS

This commit is contained in:
2026-06-09 10:05:00 -05:00
parent b19deda02c
commit d0581d36d1
9 changed files with 896 additions and 6 deletions
+157
View File
@@ -22,6 +22,7 @@ const SECRET = process.env.SESSION_SECRET || 'infinity-cambia-esta-clave-en-prod
const DATA_DIR = path.join(__dirname, 'data');
const FRONTEND_DIR = path.join(__dirname, 'frontend');
const IMG_DIR = path.join(__dirname, 'Imagenes');
const PEDIDOS_DIR = path.join(__dirname, 'pedidos');
// ════════════════════════════════
// MIDDLEWARES
@@ -30,6 +31,7 @@ app.use(express.json());
app.use(cookieParser(SECRET));
app.use(express.static(FRONTEND_DIR));
app.use('/Imagenes', express.static(IMG_DIR, { maxAge: '7d' }));
app.use('/pedidos', express.static(PEDIDOS_DIR));
// Evita el 404 de favicon.ico
app.get('/favicon.ico', (req, res) => res.status(204).end());
@@ -1886,6 +1888,161 @@ app.post('/api/descargas/fotos', requireDescargasAccess, async (req, res) => {
}
});
// ════════════════════════════════
// PEDIDOS — generar PDF con pedido
// ════════════════════════════════
app.post('/api/pedidos/generar-pdf', async (req, res) => {
const { items, cliente } = req.body || {};
if (!Array.isArray(items) || items.length === 0) {
return res.status(400).json({ error: 'El pedido debe tener al menos un producto' });
}
if (!cliente || !cliente.nombre || !cliente.ciudad || !cliente.telefono || !cliente.direccion) {
return res.status(400).json({ error: 'Todos los datos del cliente son obligatorios' });
}
try {
const filename = `pedido-${Date.now()}.pdf`;
const filepath = path.join(PEDIDOS_DIR, filename);
// Asegurar que la carpeta existe
await fs.mkdir(PEDIDOS_DIR, { recursive: true });
const doc = new PDFDocument({
size: 'A4',
margins: { top: 50, bottom: 50, left: 50, right: 50 },
info: {
Title: `Pedido - ${cliente.nombre}`,
Author: 'Infinity',
Subject: 'Pedido de productos'
}
});
const writeStream = fsSync.createWriteStream(filepath);
doc.pipe(writeStream);
const pageW = doc.page.width;
const MARGIN_X = 50;
const pageH = doc.page.height;
// === ENCABEZADO ===
doc.fontSize(36).fillColor('#8b5cf6').font('Helvetica-Bold')
.text('∞ Infinity', MARGIN_X, 50, { width: pageW - 2 * MARGIN_X });
doc.moveTo(MARGIN_X, 100).lineTo(pageW - MARGIN_X, 100)
.strokeColor('#8b5cf6').lineWidth(1.5).stroke();
doc.fontSize(22).fillColor('#1a1a2e').font('Helvetica-Bold')
.text('Pedido', MARGIN_X, 120, { width: pageW - 2 * MARGIN_X });
const fechaStr = new Date().toLocaleDateString('es-CO', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
doc.fontSize(10).fillColor('#8b8ba7').font('Helvetica')
.text(fechaStr, MARGIN_X, 150, { width: pageW - 2 * MARGIN_X });
// === DATOS DEL CLIENTE ===
doc.fontSize(14).fillColor('#1a1a2e').font('Helvetica-Bold')
.text('Datos del cliente', MARGIN_X, 185, { width: pageW - 2 * MARGIN_X });
const clientFields = [
['Nombre', cliente.nombre],
['Ciudad', cliente.ciudad],
['Teléfono', cliente.telefono],
['Dirección', cliente.direccion]
];
let clientY = 210;
doc.fontSize(11).fillColor('#4a4a68').font('Helvetica');
for (const [label, value] of clientFields) {
doc.text(`${label}: `, MARGIN_X, clientY, { continued: true });
doc.fillColor('#1a1a2e').text(value, { width: pageW - 2 * MARGIN_X });
clientY += 18;
}
// === TABLA DE PRODUCTOS ===
let tableY = clientY + 20;
doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY)
.strokeColor('#e8e6f0').lineWidth(0.5).stroke();
tableY += 12;
const colRef = MARGIN_X;
const colNombre = MARGIN_X + 80;
const colCant = pageW - MARGIN_X - 180;
const colPrecio = pageW - MARGIN_X - 120;
const colSubtotal = pageW - MARGIN_X - 60;
doc.fontSize(9).fillColor('#8b8ba7').font('Helvetica-Bold');
doc.text('REF', colRef, tableY);
doc.text('Producto', colNombre, tableY);
doc.text('Cant', colCant, tableY, { align: 'center', width: 40 });
doc.text('Precio', colPrecio, tableY, { align: 'right', width: 60 });
doc.text('Total', colSubtotal, tableY, { align: 'right', width: 60 });
const headerY = tableY;
let totalGeneral = 0;
for (const item of items) {
const subtotal = (item.precio || 0) * (item.cantidad || 1);
totalGeneral += subtotal;
tableY += 22;
if (tableY > pageH - 90) {
doc.addPage();
tableY = 60;
}
doc.fontSize(9).fillColor('#8b8ba7').font('Helvetica')
.text(String(item.ref || '—'), colRef, tableY, { width: 76 });
doc.fillColor('#1a1a2e').font('Helvetica')
.text(String(item.nombre || item.ref || '—'), colNombre, tableY, { width: colCant - colNombre - 10 });
doc.fillColor('#1a1a2e').font('Helvetica-Bold')
.text(String(item.cantidad || 1), colCant, tableY, { align: 'center', width: 40 });
doc.fillColor('#4a4a68').font('Helvetica')
.text(`$${(item.precio || 0).toLocaleString('es-CO')}`, colPrecio, tableY, { align: 'right', width: 60 });
doc.fillColor('#8b5cf6').font('Helvetica-Bold')
.text(`$${subtotal.toLocaleString('es-CO')}`, colSubtotal, tableY, { align: 'right', width: 60 });
}
// Línea final
tableY += 20;
doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY)
.strokeColor('#e8e6f0').lineWidth(0.5).stroke();
tableY += 14;
doc.fontSize(11).fillColor('#4a4a68').font('Helvetica');
doc.text('Total pedido:', MARGIN_X, tableY, { continued: true });
doc.fontSize(16).fillColor('#8b5cf6').font('Helvetica-Bold')
.text(` $${totalGeneral.toLocaleString('es-CO')}`, { width: pageW - 2 * MARGIN_X });
// === FOOTER con número de página ===
const range = doc.bufferedPageRange();
for (let i = range.start; i < range.start + range.count; i++) {
doc.switchToPage(i);
doc.fontSize(8).fillColor('#8b8ba7').font('Helvetica')
.text(`${i + 1} / ${range.count}`,
MARGIN_X, pageH - 40, { width: pageW - 2 * MARGIN_X, align: 'center' });
}
doc.end();
// Esperar a que termine de escribir
await new Promise((resolve, reject) => {
writeStream.on('finish', resolve);
writeStream.on('error', reject);
});
// Enviar el archivo
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.sendFile(filepath);
} catch (err) {
console.error('Pedido PDF error:', err);
res.status(500).json({ error: 'Error al generar el pedido: ' + err.message });
}
});
// ════════════════════════════════
// START
// ════════════════════════════════
+12 -4
View File
@@ -57,7 +57,9 @@
"dama",
"aaa"
],
"img": "bolsos/dama/aaa/AF100-197.webp"
"img": "bolsos/dama/aaa/AF100-197.webp",
"nombre": "BOLSO",
"precio": 10000
},
{
"id": "AF100-198",
@@ -67,7 +69,9 @@
"dama",
"aaa"
],
"img": "bolsos/dama/aaa/AF100-198.webp"
"img": "bolsos/dama/aaa/AF100-198.webp",
"nombre": "BOLSO",
"precio": 10000
},
{
"id": "AF100-199",
@@ -77,7 +81,9 @@
"dama",
"aaa"
],
"img": "bolsos/dama/aaa/AF100-199.webp"
"img": "bolsos/dama/aaa/AF100-199.webp",
"nombre": "BOLSO",
"precio": 10000
},
{
"id": "AF100-201",
@@ -87,7 +93,9 @@
"dama",
"aaa"
],
"img": "bolsos/dama/aaa/AF100-201.webp"
"img": "bolsos/dama/aaa/AF100-201.webp",
"nombre": "BOLSO",
"precio": 10000
},
{
"id": "AF100-202",
+2
View File
@@ -10,6 +10,8 @@ services:
# Persistencia: los cambios en JSON se guardan en el host
- ./data:/app/data
- ./Imagenes:/app/Imagenes
- ./pedidos:/app/pedidos
- ./frontend:/app/frontend
environment:
- NODE_ENV=production
- PORT=3000
+256
View File
@@ -275,10 +275,21 @@ function renderProductos() {
<h3 class="producto-nombre">${escapeHtml(nombre)}</h3>
${desc ? `<p class="producto-desc">${escapeHtml(desc)}</p>` : ''}
<div class="producto-precio">${precio}</div>
<div class="producto-card-add">
<input type="number" class="producto-card-add-qty" value="1" min="1" max="99"
data-ref="${p.ref}" aria-label="Cantidad">
<button class="producto-card-add-btn" data-ref="${p.ref}"
data-nombre="${escapeHtml(nombre)}"
data-precio="${p.precio ?? ''}">
+ Agregar
</button>
</div>
</div>
</article>
`;
}).join('');
wireAddToCart();
}
// ════════════════════════════════
@@ -328,11 +339,256 @@ function setupNav() {
});
}
// ════════════════════════════════
// CARRITO DE COMPRAS
// ════════════════════════════════
let carrito = []; // { ref, nombre, precio, cantidad }
function actualizarBadge() {
const total = carrito.reduce((s, i) => s + i.cantidad, 0);
const badge = document.getElementById('cartBadge');
badge.textContent = total;
badge.style.display = total > 0 ? 'flex' : 'none';
}
function actualizarTotal() {
const total = carrito.reduce((s, i) => s + i.precio * i.cantidad, 0);
document.querySelector('#cartTotal strong').textContent =
total > 0 ? `$${total.toLocaleString('es-CO')}` : '$0';
document.getElementById('cartPdfBtn').disabled = carrito.length === 0;
}
function renderCarritoModal() {
const body = document.getElementById('cartBody');
if (carrito.length === 0) {
body.innerHTML = '<p class="cart-empty">El carrito está vacío.</p>';
actualizarTotal();
return;
}
const totalGeneral = carrito.reduce((s, i) => s + i.precio * i.cantidad, 0);
body.innerHTML = `<div class="cart-items">
${carrito.map((item, idx) => {
const subtotal = item.precio * item.cantidad;
return `<div class="cart-item">
<div class="cart-item-info">
<div class="cart-item-name">${escapeHtml(item.nombre)}</div>
<div class="cart-item-ref">${escapeHtml(item.ref)}$${(item.precio || 0).toLocaleString('es-CO')} c/u</div>
</div>
<div class="cart-item-controls">
<button class="cart-item-qty-btn" data-cart-dec="${idx}"></button>
<span class="cart-item-qty">${item.cantidad}</span>
<button class="cart-item-qty-btn" data-cart-inc="${idx}">+</button>
</div>
<div class="cart-item-subtotal">$${subtotal.toLocaleString('es-CO')}</div>
<button class="cart-item-remove" data-cart-remove="${idx}" title="Eliminar">✕</button>
</div>`;
}).join('')}
</div>`;
body.querySelectorAll('[data-cart-inc]').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.cartInc, 10);
carrito[idx].cantidad++;
renderCarritoModal();
actualizarBadge();
});
});
body.querySelectorAll('[data-cart-dec]').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.cartDec, 10);
if (carrito[idx].cantidad > 1) {
carrito[idx].cantidad--;
} else {
carrito.splice(idx, 1);
}
renderCarritoModal();
actualizarBadge();
});
});
body.querySelectorAll('[data-cart-remove]').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.cartRemove, 10);
carrito.splice(idx, 1);
renderCarritoModal();
actualizarBadge();
});
});
actualizarTotal();
}
// Modales
function abrirModal(id) {
document.getElementById(id).classList.add('open');
document.body.style.overflow = 'hidden';
}
function cerrarModal(id) {
document.getElementById(id).classList.remove('open');
document.body.style.overflow = '';
}
function setupCartModals() {
// FAB → abrir carrito
document.getElementById('cartFab').addEventListener('click', () => {
renderCarritoModal();
abrirModal('cartModal');
});
// Cerrar carrito
document.querySelectorAll('[data-cart-close]').forEach(el => {
el.addEventListener('click', () => cerrarModal('cartModal'));
});
// Cerrar pedido
document.querySelectorAll('[data-order-close]').forEach(el => {
el.addEventListener('click', () => cerrarModal('orderModal'));
});
// Vaciar carrito
document.getElementById('cartClearBtn').addEventListener('click', () => {
carrito = [];
renderCarritoModal();
actualizarBadge();
});
// Generar PDF → abrir formulario
document.getElementById('cartPdfBtn').addEventListener('click', () => {
cerrarModal('cartModal');
document.getElementById('orderForm').reset();
document.getElementById('orderError').classList.remove('show');
abrirModal('orderModal');
});
// Enviar pedido
document.getElementById('orderSubmitBtn').addEventListener('click', enviarPedido);
// Enter en el formulario también envía
document.getElementById('orderForm').addEventListener('submit', e => {
e.preventDefault();
enviarPedido();
});
}
async function enviarPedido() {
const nombre = document.getElementById('orderNombre').value.trim();
const ciudad = document.getElementById('orderCiudad').value.trim();
const telefono = document.getElementById('orderTelefono').value.trim();
const direccion = document.getElementById('orderDireccion').value.trim();
const errEl = document.getElementById('orderError');
if (!nombre || !ciudad || !telefono || !direccion) {
errEl.textContent = 'Todos los campos son obligatorios';
errEl.classList.add('show');
return;
}
errEl.classList.remove('show');
const btn = document.getElementById('orderSubmitBtn');
const textEl = btn.querySelector('.btn-text');
const spin = btn.querySelector('.btn-spinner');
btn.disabled = true;
textEl.textContent = 'Generando…';
spin.style.display = 'inline-block';
try {
const res = await fetch('/api/pedidos/generar-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: carrito.map(i => ({ ref: i.ref, nombre: i.nombre, precio: i.precio, cantidad: i.cantidad })),
cliente: { nombre, ciudad, telefono, direccion }
})
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || 'Error al generar el pedido');
}
// Descargar PDF
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const dispo = res.headers.get('Content-Disposition') || '';
const match = dispo.match(/filename="(.+)"/);
a.download = match ? match[1] : `pedido-${Date.now()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
// Limpiar carrito y cerrar modal
carrito = [];
actualizarBadge();
cerrarModal('orderModal');
} catch (err) {
errEl.textContent = err.message;
errEl.classList.add('show');
} finally {
btn.disabled = false;
textEl.textContent = '✅ Generar pedido';
spin.style.display = 'none';
}
}
// Wire add-to-cart buttons después de renderizar productos
function wireAddToCart() {
document.querySelectorAll('.producto-card-add-btn').forEach(btn => {
if (btn.dataset.cartWired) return;
btn.dataset.cartWired = '1';
btn.addEventListener('click', () => {
const ref = btn.dataset.ref;
const nombre = btn.dataset.nombre;
const precio = parseFloat(btn.dataset.precio);
const qtyInput = document.querySelector(`.producto-card-add-qty[data-ref="${ref}"]`);
const cantidad = qtyInput ? parseInt(qtyInput.value, 10) : 1;
if (!cantidad || cantidad < 1) return;
// Mostrar feedback si el producto no tiene precio
if (!precio || isNaN(precio)) {
const orig = btn.textContent;
btn.textContent = '⚠ Sin precio';
btn.style.opacity = '0.7';
setTimeout(() => { btn.textContent = orig; btn.style.opacity = '1'; }, 1500);
return;
}
const existente = carrito.find(i => i.ref === ref);
if (existente) {
existente.cantidad += cantidad;
} else {
carrito.push({ ref, nombre, precio, cantidad });
}
if (qtyInput) qtyInput.value = 1;
actualizarBadge();
const original = btn.textContent;
btn.textContent = '✓ Agregado';
setTimeout(() => { btn.textContent = original; }, 1200);
});
});
}
// ════════════════════════════════
// INIT
// ════════════════════════════════
document.addEventListener('DOMContentLoaded', () => {
setupSearch();
setupNav();
setupCartModals();
cargarDatos();
});
+391
View File
@@ -887,3 +887,394 @@ section {
font-size: 2rem;
}
}
/* ════════════════════════════════
CARRITO DE COMPRAS
════════════════════════════════ */
/* Botón flotante del carrito */
.cart-fab {
position: fixed;
bottom: 28px;
right: 28px;
z-index: 900;
width: 60px;
height: 60px;
border-radius: 50%;
border: none;
background: linear-gradient(135deg, #8b5cf6, #a78bfa);
color: #fff;
font-size: 1.5rem;
cursor: pointer;
box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4);
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s, box-shadow 0.2s;
}
.cart-fab:hover {
transform: scale(1.08);
box-shadow: 0 6px 28px rgba(139, 92, 246, 0.55);
}
.cart-fab:active {
transform: scale(0.95);
}
.cart-fab-badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 22px;
height: 22px;
padding: 0 6px;
border-radius: 11px;
background: #ef4444;
color: #fff;
font-size: 0.7rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Plus Jakarta Sans', sans-serif;
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.4);
}
/* Modal genérico (compartido con carrito y pedido) */
.modal {
display: none;
position: fixed;
inset: 0;
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.open {
display: flex;
}
.modal-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
}
.modal-content {
position: relative;
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
max-width: 520px;
width: 90vw;
max-height: 85vh;
display: flex;
flex-direction: column;
animation: modalIn 0.25s ease-out;
}
@keyframes modalIn {
from { opacity: 0; transform: translateY(20px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px 0;
}
.modal-header h2 {
font-family: 'DM Serif Display', serif;
font-size: 1.4rem;
color: #1a1a2e;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 1.3rem;
color: #8b8ba7;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: background 0.15s;
}
.modal-close:hover {
background: #f3f0ff;
color: #1a1a2e;
}
.modal-body {
padding: 20px 24px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 16px 24px 20px;
border-top: 1px solid #e8e6f0;
display: flex;
flex-direction: column;
gap: 12px;
}
/* Carrito — lista de items */
.cart-items {
display: flex;
flex-direction: column;
gap: 10px;
}
.cart-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f8f7fc;
border-radius: 10px;
}
.cart-item-info {
flex: 1;
min-width: 0;
}
.cart-item-name {
font-size: 0.85rem;
font-weight: 600;
color: #1a1a2e;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cart-item-ref {
font-size: 0.72rem;
color: #8b8ba7;
}
.cart-item-controls {
display: flex;
align-items: center;
gap: 6px;
}
.cart-item-qty-btn {
width: 28px;
height: 28px;
border: 1px solid #d4d0e0;
border-radius: 6px;
background: #fff;
color: #1a1a2e;
font-size: 1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s;
}
.cart-item-qty-btn:hover {
background: #f3f0ff;
}
.cart-item-qty {
font-size: 0.9rem;
font-weight: 600;
color: #1a1a2e;
min-width: 24px;
text-align: center;
}
.cart-item-subtotal {
font-size: 0.85rem;
font-weight: 700;
color: #8b5cf6;
min-width: 70px;
text-align: right;
}
.cart-item-remove {
background: none;
border: none;
color: #ef4444;
cursor: pointer;
font-size: 1rem;
padding: 4px;
border-radius: 4px;
transition: background 0.15s;
}
.cart-item-remove:hover {
background: #fef2f2;
}
.cart-empty {
text-align: center;
color: #8b8ba7;
padding: 32px 0;
font-size: 0.95rem;
}
.cart-total {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 1.05rem;
color: #1a1a2e;
}
.cart-total strong {
font-size: 1.3rem;
color: #8b5cf6;
}
.cart-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
/* Formulario de pedido */
.order-modal-content {
max-width: 440px;
}
.form-group {
margin-bottom: 14px;
}
.form-group label {
display: block;
font-size: 0.82rem;
font-weight: 600;
color: #4a4a68;
margin-bottom: 4px;
}
.form-group input {
width: 100%;
padding: 10px 14px;
border: 1.5px solid #d4d0e0;
border-radius: 10px;
font-size: 0.9rem;
font-family: 'Plus Jakarta Sans', sans-serif;
color: #1a1a2e;
background: #fafaff;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: #8b5cf6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
}
.form-error {
color: #ef4444;
font-size: 0.82rem;
display: none;
margin-bottom: 8px;
}
.form-error.show {
display: block;
}
/* Botón "Agregar" en tarjetas de producto */
.producto-card-add {
display: flex;
align-items: center;
gap: 6px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #e8e6f0;
}
.producto-card-add-qty {
width: 48px;
padding: 6px 8px;
border: 1.5px solid #d4d0e0;
border-radius: 8px;
font-size: 0.82rem;
font-weight: 600;
text-align: center;
font-family: 'Plus Jakarta Sans', sans-serif;
color: #1a1a2e;
background: #fff;
transition: border-color 0.2s;
box-sizing: border-box;
}
.producto-card-add-qty:focus {
outline: none;
border-color: #8b5cf6;
}
.producto-card-add-btn {
flex: 1;
padding: 7px 12px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #8b5cf6, #a78bfa);
color: #fff;
font-size: 0.78rem;
font-weight: 600;
font-family: 'Plus Jakarta Sans', sans-serif;
cursor: pointer;
transition: opacity 0.2s, transform 0.15s;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.producto-card-add-btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.producto-card-add-btn:active {
transform: scale(0.97);
}
/* Spinner en botones */
.btn-spinner {
margin-left: 6px;
}
/* Responsive carrito */
@media (max-width: 480px) {
.cart-fab {
width: 52px;
height: 52px;
bottom: 20px;
right: 20px;
font-size: 1.3rem;
}
.modal-content {
width: 95vw;
border-radius: 12px;
}
.cart-item {
flex-wrap: wrap;
}
.cart-actions {
flex-direction: column;
}
.cart-actions .btn {
width: 100%;
justify-content: center;
}
}
+78 -2
View File
@@ -9,7 +9,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="estilos.css?v=5">
<link rel="stylesheet" href="estilos.css?v=6">
</head>
<body>
@@ -102,6 +102,82 @@
</div>
</footer>
<script src="app.js?v=5"></script>
<!-- CARRITO FLOTANTE -->
<button class="cart-fab" id="cartFab" aria-label="Abrir carrito">
<span class="cart-fab-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
</svg>
</span>
<span class="cart-fab-badge" id="cartBadge">0</span>
</button>
<!-- MODAL DEL CARRITO -->
<div class="modal" id="cartModal">
<div class="modal-backdrop" data-cart-close></div>
<div class="modal-content cart-modal-content">
<div class="modal-header">
<h2>🛒 Carrito</h2>
<button class="modal-close" data-cart-close></button>
</div>
<div class="modal-body" id="cartBody">
<p class="cart-empty">El carrito está vacío.</p>
</div>
<div class="modal-footer">
<div class="cart-total" id="cartTotal">
<span>Total:</span>
<strong>$0</strong>
</div>
<div class="cart-actions">
<button class="btn btn-ghost" id="cartClearBtn">Vaciar carrito</button>
<button class="btn btn-primary" id="cartPdfBtn" disabled>📄 Generar PDF</button>
</div>
</div>
</div>
</div>
<!-- MODAL FORMULARIO DE PEDIDO -->
<div class="modal" id="orderModal">
<div class="modal-backdrop" data-order-close></div>
<div class="modal-content order-modal-content">
<div class="modal-header">
<h2>📄 Datos del pedido</h2>
<button class="modal-close" data-order-close></button>
</div>
<div class="modal-body">
<form id="orderForm" autocomplete="off">
<div class="form-group">
<label for="orderNombre">Nombre completo</label>
<input type="text" id="orderNombre" required placeholder="Tu nombre">
</div>
<div class="form-group">
<label for="orderCiudad">Ciudad</label>
<input type="text" id="orderCiudad" required placeholder="Tu ciudad">
</div>
<div class="form-group">
<label for="orderTelefono">Teléfono</label>
<input type="tel" id="orderTelefono" required placeholder="Tu teléfono">
</div>
<div class="form-group">
<label for="orderDireccion">Dirección</label>
<input type="text" id="orderDireccion" required placeholder="Tu dirección">
</div>
<div class="form-error" id="orderError" role="alert"></div>
</form>
</div>
<div class="modal-footer">
<div class="cart-actions">
<button class="btn btn-ghost" data-order-close>Cancelar</button>
<button class="btn btn-primary" id="orderSubmitBtn">
<span class="btn-text">✅ Generar pedido</span>
<span class="btn-spinner" style="display:none"></span>
</button>
</div>
</div>
</div>
</div>
<script src="app.js?v=7"></script>
</body>
</html>
Binary file not shown.
Binary file not shown.
Binary file not shown.