Adds barcode scanner for product checkout
Implements client-side barcode scanning functionality with: - Scan button and input handling - Mock product database for EAN code lookup - Dynamic cart item addition - Visual feedback for scanning states Enhances product checkout experience with quick product entry
This commit is contained in:
parent
4ead6bb544
commit
a4fc822229
1 changed files with 325 additions and 0 deletions
|
|
@ -943,6 +943,96 @@
|
|||
border-color: #ccc;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
BARCODE SCANNER
|
||||
========================================== */
|
||||
.scan-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 14px 20px;
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-teal);
|
||||
background: color-mix(in srgb, var(--color-teal) 8%, white);
|
||||
border: 2px dashed var(--color-teal);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
.scan-btn:hover {
|
||||
background: color-mix(in srgb, var(--color-teal) 15%, white);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.scan-btn.scanning {
|
||||
border-color: #1976d2;
|
||||
color: #1976d2;
|
||||
background: color-mix(in srgb, #1976d2 8%, white);
|
||||
animation: pulse-border 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0%, 100% { border-color: #1976d2; }
|
||||
50% { border-color: #64b5f6; }
|
||||
}
|
||||
|
||||
.scan-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.scanner-input-hidden {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Debug codes in sidebar */
|
||||
.debug-codes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.debug-codes strong {
|
||||
color: var(--color-text);
|
||||
margin-bottom: 4px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.debug-codes div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.debug-codes code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
background: var(--color-surface);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.debug-codes code:hover {
|
||||
background: var(--color-teal);
|
||||
color: white;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -992,6 +1082,13 @@
|
|||
<div class="category"><span>Styling</span><span>›</span></div>
|
||||
<div class="category"><span>Olaplex</span><span>›</span></div>
|
||||
</div>
|
||||
<div class="debug-codes">
|
||||
<strong>Test EAN-koder:</strong>
|
||||
<div><code>5012345678900</code> Olaplex No.4</div>
|
||||
<div><code>8710447489109</code> Redken Shampoo</div>
|
||||
<div><code>3474636610143</code> Kérastase</div>
|
||||
<div><code>0000000000000</code> Ukendt</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cart-area">
|
||||
|
|
@ -1176,6 +1273,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scan Button -->
|
||||
<button class="scan-btn" id="scanButton">
|
||||
<svg viewBox="0 0 24 24"><path d="M2 6h2v12H2V6zm3 0h1v12H5V6zm2 0h3v12H7V6zm4 0h1v12h-1V6zm3 0h2v12h-2V6zm3 0h3v12h-3V6zm4 0h1v12h-1V6z"/></svg>
|
||||
SCAN PRODUKT
|
||||
</button>
|
||||
<!-- Hidden scanner input -->
|
||||
<input type="text" id="scannerInput" class="scanner-input-hidden" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1653,6 +1758,226 @@
|
|||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') closePanel();
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// BARCODE SCANNER
|
||||
// ==========================================
|
||||
|
||||
// Mock product database for scanning
|
||||
const scanProducts = {
|
||||
'5012345678900': {
|
||||
name: 'Olaplex No.4 Bond Maintenance Shampoo',
|
||||
price: 299,
|
||||
size: '250ml'
|
||||
},
|
||||
'8710447489109': {
|
||||
name: 'Redken All Soft Shampoo',
|
||||
price: 249,
|
||||
size: '300ml'
|
||||
},
|
||||
'3474636610143': {
|
||||
name: 'Kérastase Elixir Ultime',
|
||||
price: 425,
|
||||
size: '100ml'
|
||||
},
|
||||
'0850018802239': {
|
||||
name: 'Olaplex No.7 Bonding Oil',
|
||||
price: 319,
|
||||
size: '30ml'
|
||||
}
|
||||
};
|
||||
|
||||
// Scanner elements
|
||||
const scanButton = document.getElementById('scanButton');
|
||||
const scannerInput = document.getElementById('scannerInput');
|
||||
|
||||
let isScanning = false;
|
||||
let scannedCode = '';
|
||||
let scanTimeout = null;
|
||||
|
||||
// Start scanning
|
||||
scanButton.addEventListener('click', () => {
|
||||
if (isScanning) return;
|
||||
|
||||
isScanning = true;
|
||||
scannedCode = '';
|
||||
scanButton.classList.add('scanning');
|
||||
scanButton.innerHTML = `
|
||||
<svg viewBox="0 0 24 24"><path d="M2 6h2v12H2V6zm3 0h1v12H5V6zm2 0h3v12H7V6zm4 0h1v12h-1V6zm3 0h2v12h-2V6zm3 0h3v12h-3V6zm4 0h1v12h-1V6z"/></svg>
|
||||
SCANNING...
|
||||
`;
|
||||
scannerInput.value = '';
|
||||
scannerInput.focus();
|
||||
});
|
||||
|
||||
// Handle scanner input
|
||||
scannerInput.addEventListener('input', (e) => {
|
||||
scannedCode = e.target.value;
|
||||
|
||||
if (scanTimeout) clearTimeout(scanTimeout);
|
||||
|
||||
// After 200ms of no input, consider scan complete
|
||||
scanTimeout = setTimeout(() => {
|
||||
if (scannedCode.length >= 8) {
|
||||
processScannedCode(scannedCode);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
|
||||
// Handle Enter key from scanner
|
||||
scannerInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
if (scannedCode.length >= 8) {
|
||||
clearTimeout(scanTimeout);
|
||||
processScannedCode(scannedCode);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle blur - reset if no code scanned
|
||||
scannerInput.addEventListener('blur', () => {
|
||||
if (isScanning && scannedCode.length === 0) {
|
||||
setTimeout(() => {
|
||||
if (isScanning && scannedCode.length === 0) {
|
||||
resetScanner();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset scanner to ready state
|
||||
function resetScanner() {
|
||||
isScanning = false;
|
||||
scannedCode = '';
|
||||
scanButton.classList.remove('scanning');
|
||||
scanButton.innerHTML = `
|
||||
<svg viewBox="0 0 24 24"><path d="M2 6h2v12H2V6zm3 0h1v12H5V6zm2 0h3v12H7V6zm4 0h1v12h-1V6zm3 0h2v12h-2V6zm3 0h3v12h-3V6zm4 0h1v12h-1V6z"/></svg>
|
||||
SCAN PRODUKT
|
||||
`;
|
||||
}
|
||||
|
||||
// Process scanned code
|
||||
async function processScannedCode(code) {
|
||||
// Show loading state
|
||||
scanButton.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" style="animation: spin 1s linear infinite;"><path d="M2 6h2v12H2V6zm3 0h1v12H5V6zm2 0h3v12H7V6zm4 0h1v12h-1V6zm3 0h2v12h-2V6zm3 0h3v12h-3V6zm4 0h1v12h-1V6z"/></svg>
|
||||
HENTER...
|
||||
`;
|
||||
|
||||
// Simulate API delay
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
|
||||
// Look up product
|
||||
const foundProduct = scanProducts[code] || null;
|
||||
|
||||
if (foundProduct) {
|
||||
// Add to cart
|
||||
addScannedProductToCart(foundProduct, code);
|
||||
|
||||
// Show success briefly
|
||||
scanButton.style.borderColor = '#43a047';
|
||||
scanButton.style.color = '#43a047';
|
||||
scanButton.innerHTML = `
|
||||
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||
TILFØJET!
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
scanButton.style.borderColor = '';
|
||||
scanButton.style.color = '';
|
||||
resetScanner();
|
||||
}, 800);
|
||||
} else {
|
||||
// Show not found
|
||||
scanButton.style.borderColor = '#f59e0b';
|
||||
scanButton.style.color = '#f59e0b';
|
||||
scanButton.innerHTML = `
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
|
||||
IKKE FUNDET
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
scanButton.style.borderColor = '';
|
||||
scanButton.style.color = '';
|
||||
resetScanner();
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
// Add scanned product to cart visually
|
||||
function addScannedProductToCart(product, ean) {
|
||||
const cartSectionItems = document.querySelector('.cart-section:last-of-type .cart-section-items');
|
||||
|
||||
const newItem = document.createElement('div');
|
||||
newItem.className = 'cart-item';
|
||||
newItem.setAttribute('onclick', 'toggleCartItem(this)');
|
||||
newItem.setAttribute('data-base-price', product.price);
|
||||
newItem.innerHTML = `
|
||||
<div class="cart-item-main">
|
||||
<div class="item-qty" onclick="event.stopPropagation()">
|
||||
<button class="qty-btn">−</button>
|
||||
<span class="qty-val">1</span>
|
||||
<button class="qty-btn">+</button>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<div class="item-name">${product.name}</div>
|
||||
<div class="item-meta">${product.size}</div>
|
||||
</div>
|
||||
<div class="item-price">
|
||||
<div class="item-original-price">${product.price} kr</div>
|
||||
<div class="item-total">${product.price} kr</div>
|
||||
<div class="item-discount">-0 kr rabat</div>
|
||||
</div>
|
||||
<button class="item-remove" onclick="event.stopPropagation(); this.closest('.cart-item').remove();">✕</button>
|
||||
</div>
|
||||
<div class="cart-item-edit" onclick="event.stopPropagation()">
|
||||
<div class="edit-row">
|
||||
<div class="edit-field">
|
||||
<div class="edit-field-label">Pris</div>
|
||||
<input type="text" class="edit-field-input mono edit-price" value="${product.price}" oninput="updateCartItemDisplay(this)">
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<div class="edit-field-label">Rabat</div>
|
||||
<div class="discount-row">
|
||||
<input type="text" class="edit-field-input mono edit-discount" value="0" style="width: 80px;" oninput="updateCartItemDisplay(this)">
|
||||
<div class="discount-type">
|
||||
<button class="discount-type-btn active" data-type="kr">kr</button>
|
||||
<button class="discount-type-btn" data-type="pct">%</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert at end of items list
|
||||
cartSectionItems.appendChild(newItem);
|
||||
|
||||
// Flash the new item
|
||||
newItem.style.animation = 'flash 0.5s ease';
|
||||
|
||||
// Re-attach discount type handlers
|
||||
newItem.querySelectorAll('.discount-type-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleDiscountType(btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add animations
|
||||
const scannerStyle = document.createElement('style');
|
||||
scannerStyle.textContent = `
|
||||
@keyframes flash {
|
||||
0% { background-color: rgba(0, 137, 123, 0.2); }
|
||||
100% { background-color: transparent; }
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(scannerStyle);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue