Calendar/wwwroot/poc-checkout.html
Janus C. H. Knudsen e09048742c Adds checkout proof of concept with payment UI
Creates a responsive and interactive checkout prototype demonstrating a comprehensive payment flow

Includes:
- Multi-method payment selection
- Dynamic cart and total calculation
- Intuitive user interface for transaction management
2025-12-19 15:47:27 +01:00

968 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checkout POC</title>
<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=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--color-border: #e0e0e0;
--color-surface: #fff;
--color-background: #f5f5f5;
--color-background-hover: #f0f0f0;
--color-background-alt: #fafafa;
--color-text: #333;
--color-text-secondary: #666;
--color-teal: #00897b;
--color-red: #e53935;
--transition-fast: 150ms ease;
--font-mono: 'JetBrains Mono', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--color-background);
font-size: 14px;
color: var(--color-text);
}
.demo-trigger { padding: 20px; }
.demo-btn {
padding: 12px 24px;
background: var(--color-teal);
color: white;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
/* Overlay & Panel */
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.25);
opacity: 0;
visibility: hidden;
transition: opacity 200ms, visibility 200ms;
z-index: 100;
}
.overlay.open { opacity: 1; visibility: visible; }
.panel {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 80%;
background: var(--color-background);
transform: translateX(100%);
transition: transform 200ms ease;
display: flex;
flex-direction: column;
z-index: 101;
box-shadow: -4px 0 20px rgba(0,0,0,0.15);
}
.panel.open { transform: translateX(0); }
/* Header */
.header {
display: flex;
align-items: center;
gap: 40px;
padding: 20px 28px;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
}
.header-field { display: flex; align-items: center; gap: 10px; }
.header-label { color: var(--color-text-secondary); font-size: 13px; }
.header-value { font-weight: 500; font-size: 15px; }
.header-link { color: var(--color-teal); cursor: pointer; font-size: 13px; }
.header-select {
padding: 6px 28px 6px 10px;
border: 1px solid var(--color-border);
border-radius: 4px;
font-size: 14px;
background: var(--color-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E") no-repeat right 8px center;
appearance: none;
}
.header-close {
margin-left: auto;
background: none;
border: none;
font-size: 20px;
color: var(--color-text-secondary);
cursor: pointer;
padding: 4px 8px;
}
/* Main layout */
.main {
flex: 1;
display: grid;
grid-template-columns: 20% 1fr 35%;
overflow: hidden;
}
/* Sidebar */
.sidebar {
border-right: 1px solid var(--color-border);
display: flex;
flex-direction: column;
background: var(--color-surface);
}
.search-box {
padding: 20px;
border-bottom: 1px solid var(--color-border);
}
.search-input {
width: 100%;
padding: 12px 14px 12px 40px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 14px;
background: var(--color-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E") no-repeat 12px center;
}
.menu-section {
border-bottom: 1px solid var(--color-border);
padding: 12px 0;
}
.menu-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
font-size: 14px;
cursor: pointer;
}
.menu-item:hover { background: var(--color-background-hover); }
.menu-item.active { color: var(--color-teal); font-weight: 500; }
.categories {
flex: 1;
overflow-y: auto;
padding: 12px 0;
}
.category {
display: flex;
justify-content: space-between;
padding: 12px 20px;
font-size: 14px;
cursor: pointer;
color: var(--color-text-secondary);
}
.category:hover { background: var(--color-background-hover); color: var(--color-text); }
/* Cart area */
.cart-area {
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--color-background);
}
.cart-list {
flex: 1;
overflow-y: auto;
padding: 20px 24px;
}
.cart-section {
margin-bottom: 24px;
}
.cart-section:last-child {
margin-bottom: 0;
}
.cart-section-header {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary);
padding: 0 4px 10px;
border-bottom: 1px solid var(--color-border);
margin-bottom: 12px;
}
.cart-section-items {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Cart footer with totals */
.cart-footer {
padding: 20px 24px;
background: var(--color-surface);
border-top: 1px solid var(--color-border);
}
.cart-totals {
display: flex;
flex-direction: column;
gap: 8px;
}
.cart-total-row {
display: flex;
justify-content: space-between;
font-size: 14px;
}
.cart-total-row .label {
color: var(--color-text-secondary);
}
.cart-total-row .value {
font-family: var(--font-mono);
font-weight: 500;
}
.cart-total-row.grand {
padding-top: 12px;
border-top: 1px solid var(--color-border);
margin-top: 4px;
}
.cart-total-row.grand .label {
font-size: 16px;
font-weight: 600;
color: var(--color-text);
}
.cart-total-row.grand .value {
font-family: var(--font-mono);
font-size: 24px;
font-weight: 700;
}
/* Cart item card */
.cart-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
background: var(--color-surface);
border-radius: 6px;
}
.item-qty {
display: flex;
align-items: center;
gap: 4px;
}
.qty-btn {
width: 26px;
height: 26px;
border: 1px solid var(--color-border);
background: var(--color-surface);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: var(--color-text-secondary);
display: flex;
align-items: center;
justify-content: center;
}
.qty-btn:hover { background: var(--color-background-hover); }
.qty-val {
width: 24px;
text-align: center;
font-size: 14px;
font-weight: 500;
}
.item-info {
flex: 1;
}
.item-name {
font-size: 14px;
font-weight: 500;
}
.item-meta {
font-size: 12px;
color: var(--color-text-secondary);
}
.item-price {
text-align: right;
}
.item-total {
font-family: var(--font-mono);
font-size: 15px;
font-weight: 600;
}
.item-remove {
background: none;
border: none;
font-size: 16px;
color: var(--color-text-secondary);
cursor: pointer;
opacity: 0.3;
padding: 4px;
}
.item-remove:hover { opacity: 1; color: var(--color-red); }
/* Payment Panel */
.payment-panel {
background: var(--color-surface);
border-left: 1px solid var(--color-border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.payment-total {
padding: 24px 28px;
text-align: center;
border-bottom: 1px solid var(--color-border);
}
.payment-total-amount {
font-family: var(--font-mono);
font-size: 48px;
font-weight: 700;
color: var(--color-text);
letter-spacing: -2px;
}
.payment-total-label {
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.payment-methods {
display: flex;
gap: 8px;
padding: 16px 24px;
border-bottom: 1px solid var(--color-border);
flex-wrap: wrap;
}
.method-btn {
flex: 1 1 auto;
min-width: 70px;
padding: 12px 8px;
border: 2px solid var(--color-border);
border-radius: 6px;
background: var(--color-surface);
cursor: pointer;
text-align: center;
transition: all var(--transition-fast);
}
.method-btn:hover {
border-color: #ccc;
}
.method-btn.active {
border-color: var(--color-teal);
background: rgba(0, 137, 123, 0.05);
}
.method-btn-icon {
width: 24px;
height: 24px;
display: block;
margin: 0 auto 4px;
filter: invert(22%) sepia(14%) saturate(1042%) hue-rotate(164deg) brightness(102%) contrast(85%);
}
.method-btn-label {
font-size: 11px;
color: var(--color-text-secondary);
}
.method-btn.active .method-btn-label {
color: var(--color-teal);
font-weight: 500;
}
.payment-input-section {
padding: 16px 24px;
border-bottom: 1px solid var(--color-border);
}
.payment-input-label {
font-size: 11px;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
.payment-input-row {
display: flex;
gap: 10px;
}
.payment-input {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-family: var(--font-mono);
font-size: 22px;
font-weight: 600;
text-align: right;
}
.payment-input:focus {
outline: none;
border-color: var(--color-teal);
}
.btn-pay-rest {
padding: 10px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-surface);
font-size: 11px;
color: var(--color-text-secondary);
cursor: pointer;
white-space: nowrap;
}
.btn-pay-rest:hover {
border-color: var(--color-teal);
color: var(--color-teal);
}
.btn-add-payment {
width: 100%;
padding: 12px;
margin-top: 12px;
border: none;
border-radius: 6px;
background: var(--color-teal);
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.btn-add-payment:hover {
background: #00796b;
}
.btn-add-payment:disabled {
background: #ccc;
cursor: not-allowed;
}
/* Registered payments */
.registered-payments {
flex: 1;
overflow-y: auto;
padding: 16px 24px;
}
.registered-payments-label {
font-size: 11px;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.payment-entry {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid var(--color-border);
}
.payment-entry-icon {
width: 20px;
height: 20px;
filter: invert(22%) sepia(14%) saturate(1042%) hue-rotate(164deg) brightness(102%) contrast(85%);
}
.payment-entry-info {
flex: 1;
}
.payment-entry-method {
font-size: 14px;
font-weight: 500;
}
.payment-entry-detail {
font-size: 12px;
color: var(--color-text-secondary);
}
.payment-entry-amount {
font-family: var(--font-mono);
font-size: 16px;
font-weight: 600;
color: var(--color-teal);
}
.payment-entry-remove {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
font-size: 16px;
opacity: 0.5;
padding: 4px;
}
.payment-entry-remove:hover {
opacity: 1;
color: var(--color-red);
}
.no-payments {
color: var(--color-text-secondary);
font-size: 13px;
text-align: center;
padding: 24px;
}
/* Payment footer */
.payment-footer {
padding: 16px 24px;
border-top: 1px solid var(--color-border);
background: var(--color-background-alt);
}
.remaining-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.remaining-label {
font-size: 14px;
color: var(--color-text-secondary);
}
.remaining-value {
font-family: var(--font-mono);
font-size: 28px;
font-weight: 700;
color: var(--color-red);
}
.remaining-value.zero {
color: var(--color-teal);
}
.btn-complete {
width: 100%;
padding: 16px;
border: none;
border-radius: 6px;
background: var(--color-teal);
color: white;
font-size: 16px;
font-weight: 500;
cursor: pointer;
}
.btn-complete:hover {
background: #00796b;
}
.btn-complete:disabled {
background: #ccc;
cursor: not-allowed;
}
.payment-actions {
display: flex;
gap: 12px;
margin-top: 16px;
}
.btn-secondary {
flex: 1;
padding: 10px;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-surface);
font-size: 13px;
color: var(--color-text-secondary);
cursor: pointer;
}
.btn-secondary:hover {
border-color: #ccc;
}
</style>
</head>
<body>
<div class="demo-trigger">
<button class="demo-btn" onclick="openPanel()">Gå til betaling</button>
</div>
<div class="overlay" id="overlay" onclick="closePanel()"></div>
<div class="panel" id="panel">
<div class="header">
<div class="header-field">
<span class="header-label">Kunde</span>
<span class="header-value">Sofie Nielsen</span>
<span class="header-link">Fjern</span>
</div>
<div class="header-field">
<span class="header-label">Dato</span>
<span class="header-value">16. dec 2025</span>
</div>
<div class="header-field">
<span class="header-label">Betjent af</span>
<select class="header-select">
<option>Emma Larsen</option>
<option>Anett Davidsson</option>
</select>
</div>
<button class="header-close" onclick="closePanel()"></button>
</div>
<div class="main">
<div class="sidebar">
<div class="search-box">
<input type="text" class="search-input" placeholder="Søg varer...">
</div>
<div class="menu-section">
<div class="menu-item"><span>Brugerdefineret linje</span></div>
<div class="menu-item"><span>Tidligere salg</span></div>
<div class="menu-item"><span>Refunder</span></div>
<div class="menu-item active"><span>Services</span></div>
</div>
<div class="categories">
<div class="category"><span>Acidic Bonding</span><span></span></div>
<div class="category"><span>Amino Mint</span><span></span></div>
<div class="category"><span>Color Gloss</span><span></span></div>
<div class="category"><span>Conditioner</span><span></span></div>
<div class="category"><span>Styling</span><span></span></div>
<div class="category"><span>Olaplex</span><span></span></div>
</div>
</div>
<div class="cart-area">
<div class="cart-list">
<!-- Services -->
<div class="cart-section">
<div class="cart-section-header">Services</div>
<div class="cart-section-items">
<div class="cart-item">
<div class="item-qty">
<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">Dameklip</div>
<div class="item-meta">Emma Larsen · 45 min</div>
</div>
<div class="item-price">
<div class="item-total">725 kr</div>
</div>
<button class="item-remove"></button>
</div>
<div class="cart-item">
<div class="item-qty">
<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">Gloss extra langt/tykt hår</div>
<div class="item-meta">Emma Larsen · 90 min</div>
</div>
<div class="item-price">
<div class="item-total">900 kr</div>
</div>
<button class="item-remove"></button>
</div>
</div>
</div>
<!-- Produkter -->
<div class="cart-section">
<div class="cart-section-header">Produkter</div>
<div class="cart-section-items">
<div class="cart-item">
<div class="item-qty">
<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">Olaplex No. 3</div>
<div class="item-meta">100ml</div>
</div>
<div class="item-price">
<div class="item-total">300 kr</div>
</div>
<button class="item-remove"></button>
</div>
<div class="cart-item">
<div class="item-qty">
<button class="qty-btn"></button>
<span class="qty-val">2</span>
<button class="qty-btn">+</button>
</div>
<div class="item-info">
<div class="item-name">India supercharged mask</div>
<div class="item-meta">250ml</div>
</div>
<div class="item-price">
<div class="item-total">350 kr</div>
</div>
<button class="item-remove"></button>
</div>
</div>
</div>
</div>
<div class="cart-footer">
<div class="cart-totals">
<div class="cart-total-row">
<span class="label">Subtotal</span>
<span class="value">1.820 kr</span>
</div>
<div class="cart-total-row">
<span class="label">Moms (25%)</span>
<span class="value">455 kr</span>
</div>
<div class="cart-total-row grand">
<span class="label">Total</span>
<span class="value">2.275 kr</span>
</div>
</div>
</div>
</div>
<!-- Payment Panel -->
<div class="payment-panel">
<div class="payment-total">
<div class="payment-total-amount" id="totalAmount">2.275 kr</div>
<div class="payment-total-label">Total at betale</div>
</div>
<div class="payment-methods">
<button class="method-btn active" onclick="selectMethod('kort')">
<img class="method-btn-icon" src="icons/credit-card.svg" alt="">
<span class="method-btn-label">Kort</span>
</button>
<button class="method-btn" onclick="selectMethod('kontant')">
<img class="method-btn-icon" src="icons/coins.svg" alt="">
<span class="method-btn-label">Kontant</span>
</button>
<button class="method-btn" onclick="selectMethod('mobilepay')">
<img class="method-btn-icon" src="icons/mobilepay.svg" alt="">
<span class="method-btn-label">MobilePay</span>
</button>
<button class="method-btn" onclick="selectMethod('bank')">
<img class="method-btn-icon" src="icons/bank.svg" alt="">
<span class="method-btn-label">Bank</span>
</button>
<button class="method-btn" onclick="selectMethod('gavekort')">
<img class="method-btn-icon" src="icons/gift-card.svg" alt="">
<span class="method-btn-label">Gavekort</span>
</button>
<button class="method-btn" onclick="selectMethod('tilgode')">
<img class="method-btn-icon" src="icons/loan.svg" alt="">
<span class="method-btn-label">Tilgode</span>
</button>
</div>
<div class="payment-input-section">
<div class="payment-input-label" id="inputLabel">Beløb at betale med Kort</div>
<div class="payment-input-row">
<input type="text" class="payment-input" id="paymentInput" value="2.275" inputmode="decimal">
<button class="btn-pay-rest" onclick="payRest()">Betal rest<br><span id="restAmount">(2.275 kr)</span></button>
</div>
<button class="btn-add-payment" onclick="addPayment()">+ Tilføj betaling</button>
</div>
<div class="registered-payments">
<div class="registered-payments-label">Registrerede betalinger</div>
<div id="paymentsList">
<div class="no-payments">Ingen betalinger registreret</div>
</div>
</div>
<div class="payment-footer">
<div class="remaining-row">
<span class="remaining-label">Restbeløb</span>
<span class="remaining-value" id="remainingAmount">2.275 kr</span>
</div>
<button class="btn-complete" id="btnComplete" disabled>Afslut salg</button>
<div class="payment-actions">
<button class="btn-secondary">Print kvittering</button>
<button class="btn-secondary">Send på mail</button>
</div>
</div>
</div>
</div>
</div>
<script>
const total = 2275;
const payments = [];
let currentMethod = 'kort';
const methodLabels = {
'kort': 'Kort',
'kontant': 'Kontant',
'mobilepay': 'MobilePay',
'bank': 'Bank',
'gavekort': 'Gavekort',
'tilgode': 'Tilgode'
};
const methodIcons = {
'kort': 'icons/credit-card.svg',
'kontant': 'icons/coins.svg',
'mobilepay': 'icons/mobilepay.svg',
'bank': 'icons/bank.svg',
'gavekort': 'icons/gift-card.svg',
'tilgode': 'icons/loan.svg'
};
function openPanel() {
document.getElementById('overlay').classList.add('open');
document.getElementById('panel').classList.add('open');
}
function closePanel() {
document.getElementById('overlay').classList.remove('open');
document.getElementById('panel').classList.remove('open');
}
function selectMethod(method) {
currentMethod = method;
// Update active button
document.querySelectorAll('.method-btn').forEach(btn => btn.classList.remove('active'));
event.currentTarget.classList.add('active');
// Update label
document.getElementById('inputLabel').textContent = `Beløb at betale med ${methodLabels[method]}`;
// Set input to remaining amount
payRest();
}
function payRest() {
const remaining = getRemaining();
document.getElementById('paymentInput').value = formatNumber(remaining);
}
function addPayment() {
const amount = parseAmount(document.getElementById('paymentInput').value);
if (amount <= 0) return;
payments.push({
method: currentMethod,
amount: amount
});
updatePaymentsList();
updateTotals();
// Reset input to new remaining
payRest();
}
function removePayment(index) {
payments.splice(index, 1);
updatePaymentsList();
updateTotals();
payRest();
}
function updatePaymentsList() {
const container = document.getElementById('paymentsList');
if (payments.length === 0) {
container.innerHTML = '<div class="no-payments">Ingen betalinger registreret</div>';
return;
}
container.innerHTML = payments.map((p, i) => `
<div class="payment-entry">
<img class="payment-entry-icon" src="${methodIcons[p.method]}" alt="">
<div class="payment-entry-info">
<div class="payment-entry-method">${methodLabels[p.method]}</div>
</div>
<span class="payment-entry-amount">+${formatNumber(p.amount)} kr</span>
<button class="payment-entry-remove" onclick="removePayment(${i})">✕</button>
</div>
`).join('');
}
function updateTotals() {
const remaining = getRemaining();
// Update remaining display
const remainingEl = document.getElementById('remainingAmount');
remainingEl.textContent = formatNumber(remaining) + ' kr';
// Update rest button
document.getElementById('restAmount').textContent = `(${formatNumber(remaining)} kr)`;
// Toggle styling and button state
const btnComplete = document.getElementById('btnComplete');
if (remaining <= 0) {
remainingEl.classList.add('zero');
btnComplete.disabled = false;
} else {
remainingEl.classList.remove('zero');
btnComplete.disabled = true;
}
}
function getRemaining() {
const paid = payments.reduce((sum, p) => sum + p.amount, 0);
return Math.max(0, total - paid);
}
function formatNumber(num) {
return num.toLocaleString('da-DK');
}
function parseAmount(str) {
if (!str) return 0;
const cleaned = str.replace(/\./g, '').replace(',', '.').replace(/[^0-9.]/g, '');
return parseFloat(cleaned) || 0;
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closePanel();
});
</script>
</body>
</html>