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:
Janus C. H. Knudsen 2026-01-03 18:57:57 +01:00
parent 4ead6bb544
commit a4fc822229

View file

@ -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>