More Forms Snippets
Checkout Payment Form — Card UI HTML CSS JS Snippet
Checkout Payment Form · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
/(\d{4})(?=\d)/g inserts spaces every 4 digits — turns raw input into the familiar "1234 5678" format in real-time..valid) on correct input, red border (.invalid) on partial/invalid — fires on oninput for immediate positive feedback.applyPromo() updates the discount row, total row, and pay button label simultaneously — ensures all three stay in sync.position: sticky; top: 20px on the summary panel — stays visible while the form scrolls on tall viewports.About this UI Snippet
Checkout Form — Card Number Formatting, Expiry Mask, Card Type Detection & Order Summary

The checkout form is the most conversion-critical UI component in e-commerce — every friction point costs revenue. A production-quality checkout must handle card number formatting (auto-spaces every 4 digits), expiry masking (MM / YY format), CVV restriction (digits only), card type detection (Visa/Mastercard/Amex from first digit), inline field validation, loading state, success state, and an order summary with promo code input. This snippet delivers all of these in a two-panel checkout layout.
Checkout UX is one of the most studied areas of e-commerce conversion optimisation. According to Baymard Institute, 18% of cart abandonment is caused by "too long / complicated checkout process." The techniques in this snippet directly address the most common friction points: card number spacing (reduces input errors), live field validation (catches errors before submit), and a visible order summary (confirms what the user is paying for).
Card number auto-formatting
The formatCard function strips non-digits from the input, slices to 16 characters, then applies a regex replacement /(\d{4})(?=\d)/g to insert a space after every 4th digit. The result is the 1234 5678 9012 3456 format users expect from physical card numbers. The function also detects the card type from the first digit: 4 → Visa, 5 → Mastercard, 3 → Amex. The type indicator updates inline in the card number field, providing immediate feedback on which network the card belongs to.
Expiry date masking
The formatExpiry function strips non-digits, takes the first 4, and inserts / after position 2. The user types "1226" and sees "12 / 26" — the format that exactly matches the physical card. The maxlength="7" attribute limits input to "MM / YY" (7 characters including spaces and slash).
Inline validation with visual state
Each field gets .valid (green border) or .invalid (red border) class toggled on input. Email validation uses a simple regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/. Text fields validate as valid when length ≥ 2. The validation fires on oninput (not onblur) to give immediate positive feedback as the user types, which has been shown to reduce form abandonment compared to blur-only validation.
Promo code with order total update
The promo code input triggers an applyPromo function that checks for known codes and updates the discount row and total in the order summary. The pay button label also updates to reflect the new total — ensuring the amount shown on the button always matches the order summary. In production, promo validation would be a server-side API call. Pair with a modal for a promo code success confirmation overlay.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA two-column checkout appears: form on the left (contact, delivery, payment), order summary on the right with two line items and a total.
- 2Type a card numberNumbers auto-format into groups of 4 (1234 5678...). Starting with 4 shows "VISA" in the field; 5 shows "MC"; 3 shows "AMEX".
- 3Type the expiry dateType "1226" — it formats to "12 / 26" automatically. Try any MM YY combination.
- 4Enter the promo codeType
FIRST20in the promo field and click Apply — the discount updates to −$17.80 and the pay button changes to "Pay $71.20". - 5Click PayThe button shows a spinning loader for 2.2 seconds, then changes to "✓ Order confirmed!" with a green gradient.
- 6Check inline validationType a partial email address — the field border turns red. Complete the email — it turns green. Same for all text fields with 2+ characters.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Replace the mock submitOrder with Stripe Elements or Stripe.js. Use stripe.createPaymentMethod({type: 'card', card: elements.getElement('card')}) to tokenise the card. Send the paymentMethod.id to your server, create a PaymentIntent via the Stripe API, and confirm it client-side. Never send raw card numbers to your server.
Add a checkbox "Same as delivery address" above the payment section. When checked, copy the delivery fields to hidden billing fields on submit. When unchecked, show the billing address fields below the payment section.
Add a validateAll() function that checks all required fields. Call it in submitOrder() before the loading state. Collect all invalid field IDs, focus the first one, and return early if any are invalid. Show a summary error message above the pay button listing what needs to be completed.
Use React Hook Form (useForm) for field management. Each input uses register('fieldName', {required: true, pattern: ...}). Card formatting uses onChange handlers calling the format functions. The pay button state is const [status, setStatus] = useState('idle') — 'idle', 'loading', 'success'. The order summary is a separate OrderSummary component accepting {items, discount, total} props.
Checkout Payment Form — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Checkout Payment Form</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#f8fafc;min-height:100vh;padding:28px 16px}
.checkout-page{max-width:900px;margin:0 auto}
.checkout-wrap{display:grid;grid-template-columns:1fr 340px;gap:32px;align-items:start}
.checkout-title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:20px}
.form-section{margin-bottom:24px}
.section-label{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#94a3b8;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.section-label::after{content:'';flex:1;height:1px;background:#e2e8f0}
.field-row{display:flex;flex-direction:column;gap:12px;margin-bottom:12px}
.field-row.two-col{flex-direction:row}
.field-row.two-col .field{flex:1;min-width:0}
.field{display:flex;flex-direction:column;gap:4px}
.field-label{font-size:12px;font-weight:600;color:#374151}
.field-input{padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:13px;color:#1e293b;outline:none;transition:border-color .15s,box-shadow .15s;font-family:inherit;background:#fff}
.field-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.12)}
.field-input.valid{border-color:#10b981}
.field-input.invalid{border-color:#ef4444}
.field-error{font-size:11px;color:#ef4444;min-height:14px}
.card-icons{display:flex;align-items:center;gap:6px;margin-bottom:10px;flex-wrap:wrap}
.card-icon{font-size:9px;font-weight:800;padding:3px 7px;border-radius:4px}
.card-icon.visa{background:#1a1f71;color:#fff}
.card-icon.mc{background:#eb001b;color:#fff}
.card-icon.amex{background:#007bc1;color:#fff}
.card-icon.lock{color:#64748b;font-size:10px;background:#f1f5f9;border-radius:6px;padding:3px 8px}
.card-input-wrap{position:relative}
.card-num-input{width:100%;padding-right:60px}
.card-type-indicator{position:absolute;right:10px;top:50%;transform:translateY(-50%);font-size:10px;font-weight:700;color:#6366f1}
.pay-btn{width:100%;padding:13px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:15px;font-weight:700;border:none;border-radius:12px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;font-family:inherit;margin-top:4px}
.pay-btn:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(99,102,241,.4)}
.pay-btn:active{transform:scale(.98)}
.pay-btn:disabled{opacity:.7;cursor:not-allowed;transform:none}
.spinner{animation:spin 1s linear infinite}
@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
.hidden{display:none}
.secure-note{font-size:11px;color:#94a3b8;text-align:center;margin-top:10px}
/* Order summary */
.order-summary{background:#fff;border:1.5px solid #e2e8f0;border-radius:16px;padding:22px;position:sticky;top:20px}
.summary-title{font-size:14px;font-weight:700;color:#1e293b;margin-bottom:16px}
.summary-items{display:flex;flex-direction:column;gap:12px;margin-bottom:16px}
.summary-item{display:flex;align-items:center;gap:10px}
.item-thumb{width:36px;height:36px;border-radius:8px;flex-shrink:0}
.item-info{flex:1;min-width:0}
.item-name{font-size:12px;font-weight:600;color:#1e293b}
.item-desc{font-size:11px;color:#64748b}
.item-price{font-size:13px;font-weight:700;color:#1e293b;white-space:nowrap}
.summary-divider{height:1px;background:#f1f5f9;margin-bottom:10px}
.summary-row{display:flex;justify-content:space-between;font-size:13px;color:#64748b;margin-bottom:6px}
.summary-row.total{font-size:15px;font-weight:800;color:#1e293b;margin-top:6px;padding-top:6px;border-top:1.5px solid #e2e8f0}
.discount{color:#10b981;font-weight:600}
.promo-field{display:flex;gap:6px;margin-top:14px}
.promo-input{flex:1;padding:8px 10px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:12px;color:#1e293b;outline:none;transition:border-color .15s;font-family:inherit}
.promo-input:focus{border-color:#6366f1}
.promo-btn{padding:8px 14px;background:#1e293b;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer;transition:background .15s;font-family:inherit;white-space:nowrap}
.promo-btn:hover{background:#334155}
.trust-row{display:flex;flex-direction:column;gap:5px;margin-top:14px;padding-top:12px;border-top:1px solid #f1f5f9}
.trust-item{font-size:11px;color:#10b981;font-weight:600}
@media(max-width:700px){.checkout-wrap{grid-template-columns:1fr}.order-summary{position:static;order:-1}.field-row.two-col{flex-direction:column}}
</style>
</head>
<body>
<div class="checkout-page">
<div class="checkout-wrap">
<!-- Left: Form -->
<div class="checkout-form-col">
<h1 class="checkout-title">Checkout</h1>
<section class="form-section">
<h2 class="section-label">Contact</h2>
<div class="field-row">
<div class="field">
<label class="field-label" for="email">Email</label>
<input class="field-input" type="email" id="email" placeholder="[email protected]" oninput="validate(this)">
<span class="field-error" id="emailError"></span>
</div>
</div>
</section>
<section class="form-section">
<h2 class="section-label">Delivery</h2>
<div class="field-row two-col">
<div class="field">
<label class="field-label" for="fname">First name</label>
<input class="field-input" type="text" id="fname" placeholder="Alex" oninput="validate(this)">
</div>
<div class="field">
<label class="field-label" for="lname">Last name</label>
<input class="field-input" type="text" id="lname" placeholder="Morgan" oninput="validate(this)">
</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label" for="address">Street address</label>
<input class="field-input" type="text" id="address" placeholder="123 Main St" oninput="validate(this)">
</div>
</div>
<div class="field-row two-col">
<div class="field">
<label class="field-label" for="city">City</label>
<input class="field-input" type="text" id="city" placeholder="San Francisco" oninput="validate(this)">
</div>
<div class="field">
<label class="field-label" for="zip">ZIP / Postcode</label>
<input class="field-input" type="text" id="zip" placeholder="94102" oninput="validate(this)">
</div>
</div>
</section>
<section class="form-section">
<h2 class="section-label">Payment</h2>
<div class="card-icons">
<div class="card-icon visa">VISA</div>
<div class="card-icon mc">MC</div>
<div class="card-icon amex">AMEX</div>
<div class="card-icon lock">🔒 Secure</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label" for="cardNum">Card number</label>
<div class="card-input-wrap">
<input class="field-input card-num-input" type="text" id="cardNum" placeholder="1234 5678 9012 3456" maxlength="19" oninput="formatCard(this)">
<div class="card-type-indicator" id="cardTypeIndicator"></div>
</div>
</div>
</div>
<div class="field-row two-col">
<div class="field">
<label class="field-label" for="expiry">Expiry</label>
<input class="field-input" type="text" id="expiry" placeholder="MM / YY" maxlength="7" oninput="formatExpiry(this)">
</div>
<div class="field">
<label class="field-label" for="cvv">CVV</label>
<input class="field-input" type="text" id="cvv" placeholder="•••" maxlength="4" oninput="this.value=this.value.replace(/\D/g,'')">
</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label" for="cardName">Name on card</label>
<input class="field-input" type="text" id="cardName" placeholder="ALEX MORGAN">
</div>
</div>
</section>
<button class="pay-btn" id="payBtn" onclick="submitOrder()">
<span id="payBtnText">Pay $89.00</span>
<svg id="payBtnSpinner" class="spinner hidden" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10" stroke-opacity=".3"/><path d="M12 2a10 10 0 0 1 10 10"/></svg>
</button>
<p class="secure-note">🔒 Payments secured by 256-bit SSL encryption.</p>
</div>
<!-- Right: Order summary -->
<div class="order-summary">
<h2 class="summary-title">Order Summary</h2>
<div class="summary-items">
<div class="summary-item">
<div class="item-thumb" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)"></div>
<div class="item-info">
<div class="item-name">FWD Tools Pro Plan</div>
<div class="item-desc">12 months · Unlimited access</div>
</div>
<div class="item-price">$79.00</div>
</div>
<div class="summary-item">
<div class="item-thumb" style="background:linear-gradient(135deg,#10b981,#059669)"></div>
<div class="item-info">
<div class="item-name">Component Export Add-on</div>
<div class="item-desc">React + Vue + Angular</div>
</div>
<div class="item-price">$10.00</div>
</div>
</div>
<div class="summary-divider"></div>
<div class="summary-row"><span>Subtotal</span><span>$89.00</span></div>
<div class="summary-row"><span>Discount (FIRST20)</span><span class="discount">−$0.00</span></div>
<div class="summary-row"><span>Tax</span><span>$0.00</span></div>
<div class="summary-row total"><span>Total</span><span>$89.00</span></div>
<div class="promo-field">
<input type="text" class="promo-input" id="promoInput" placeholder="Promo code">
<button class="promo-btn" onclick="applyPromo()">Apply</button>
</div>
<div class="trust-row">
<span class="trust-item">✓ 30-day money back</span>
<span class="trust-item">✓ Instant access</span>
<span class="trust-item">✓ Cancel anytime</span>
</div>
</div>
</div>
</div>
<script>
function formatCard(input) {
let val = input.value.replace(/\D/g, '').slice(0, 16);
input.value = val.replace(/(\d{4})(?=\d)/g, '$1 ');
const indicator = document.getElementById('cardTypeIndicator');
if (val.startsWith('4')) indicator.textContent = 'VISA';
else if (val.startsWith('5')) indicator.textContent = 'MC';
else if (val.startsWith('3')) indicator.textContent = 'AMEX';
else indicator.textContent = '';
}
function formatExpiry(input) {
let val = input.value.replace(/\D/g, '').slice(0, 4);
if (val.length >= 2) val = val.slice(0, 2) + ' / ' + val.slice(2);
input.value = val;
}
function validate(input) {
const val = input.value.trim();
if (input.type === 'email') {
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
input.classList.toggle('valid', valid && val.length > 0);
input.classList.toggle('invalid', !valid && val.length > 0);
} else {
input.classList.toggle('valid', val.length >= 2);
input.classList.toggle('invalid', val.length === 1);
}
}
function applyPromo() {
const code = document.getElementById('promoInput').value.trim().toUpperCase();
if (code === 'FIRST20') {
document.querySelector('.discount').textContent = '−$17.80';
document.querySelector('.summary-row.total span:last-child').textContent = '$71.20';
document.getElementById('payBtn').querySelector('#payBtnText').textContent = 'Pay $71.20';
}
}
function submitOrder() {
const btn = document.getElementById('payBtn');
const text = document.getElementById('payBtnText');
const spinner = document.getElementById('payBtnSpinner');
btn.disabled = true;
text.textContent = 'Processing…';
spinner.classList.remove('hidden');
setTimeout(() => {
spinner.classList.add('hidden');
text.textContent = '✓ Order confirmed!';
btn.style.background = 'linear-gradient(135deg,#10b981,#059669)';
}, 2200);
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Checkout Payment Form</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,sans-serif;background:#f8fafc;min-height:100vh;padding:28px 16px
}
.section-label::after {
content:'';flex:1;height:1px;background:#e2e8f0
}
.field-row.two-col {
flex-direction:row
}
.field-row.two-col .field {
flex:1;min-width:0
}
.field-input.valid {
border-color:#10b981
}
.field-input.invalid {
border-color:#ef4444
}
.card-icon.visa {
background:#1a1f71;color:#fff
}
.card-icon.mc {
background:#eb001b;color:#fff
}
.card-icon.amex {
background:#007bc1;color:#fff
}
.card-icon.lock {
color:#64748b;font-size:10px;background:#f1f5f9;border-radius:6px;padding:3px 8px
}
.pay-btn:disabled {
opacity:.7;cursor:not-allowed;transform:none
}
.hidden {
display:none
}
.summary-row.total {
font-size:15px;font-weight:800;color:#1e293b;margin-top:6px;padding-top:6px;border-top:1.5px solid #e2e8f0
}
.field-row.two-col {
flex-direction:column
}
</style>
</head>
<body>
<div class="max-w-[900px] my-0 mx-auto">
<div class="grid grid-cols-1 gap-8 items-start max-[700px]:grid-cols-1">
<!-- Left: Form -->
<div class="checkout-form-col">
<h1 class="text-[22px] font-extrabold text-[#1e293b] mb-5">Checkout</h1>
<section class="mb-6">
<h2 class="section-label text-[11px] font-bold tracking-[.07em] uppercase text-[#94a3b8] mb-3 flex items-center gap-2">Contact</h2>
<div class="field-row flex flex-col gap-3 mb-3">
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="email">Email</label>
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="email" id="email" placeholder="[email protected]" oninput="validate(this)">
<span class="text-[11px] text-[#ef4444] min-h-[14px]" id="emailError"></span>
</div>
</div>
</section>
<section class="mb-6">
<h2 class="section-label text-[11px] font-bold tracking-[.07em] uppercase text-[#94a3b8] mb-3 flex items-center gap-2">Delivery</h2>
<div class="field-row flex flex-col gap-3 mb-3 two-col">
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="fname">First name</label>
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="fname" placeholder="Alex" oninput="validate(this)">
</div>
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="lname">Last name</label>
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="lname" placeholder="Morgan" oninput="validate(this)">
</div>
</div>
<div class="field-row flex flex-col gap-3 mb-3">
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="address">Street address</label>
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="address" placeholder="123 Main St" oninput="validate(this)">
</div>
</div>
<div class="field-row flex flex-col gap-3 mb-3 two-col">
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="city">City</label>
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="city" placeholder="San Francisco" oninput="validate(this)">
</div>
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="zip">ZIP / Postcode</label>
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="zip" placeholder="94102" oninput="validate(this)">
</div>
</div>
</section>
<section class="mb-6">
<h2 class="section-label text-[11px] font-bold tracking-[.07em] uppercase text-[#94a3b8] mb-3 flex items-center gap-2">Payment</h2>
<div class="flex items-center gap-1.5 mb-2.5 flex-wrap">
<div class="card-icon text-[9px] font-extrabold py-[3px] px-[7px] rounded visa">VISA</div>
<div class="card-icon text-[9px] font-extrabold py-[3px] px-[7px] rounded mc">MC</div>
<div class="card-icon text-[9px] font-extrabold py-[3px] px-[7px] rounded amex">AMEX</div>
<div class="card-icon text-[9px] font-extrabold py-[3px] px-[7px] rounded lock">🔒 Secure</div>
</div>
<div class="field-row flex flex-col gap-3 mb-3">
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="cardNum">Card number</label>
<div class="relative">
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)] w-full pr-[60px]" type="text" id="cardNum" placeholder="1234 5678 9012 3456" maxlength="19" oninput="formatCard(this)">
<div class="absolute right-2.5 top-1/2 [transform:translateY(-50%)] text-xs font-bold text-[#6366f1]" id="cardTypeIndicator"></div>
</div>
</div>
</div>
<div class="field-row flex flex-col gap-3 mb-3 two-col">
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="expiry">Expiry</label>
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="expiry" placeholder="MM / YY" maxlength="7" oninput="formatExpiry(this)">
</div>
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="cvv">CVV</label>
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="cvv" placeholder="•••" maxlength="4" oninput="this.value=this.value.replace(/\D/g,'')">
</div>
</div>
<div class="field-row flex flex-col gap-3 mb-3">
<div class="field flex flex-col gap-1">
<label class="text-xs font-semibold text-[#374151]" for="cardName">Name on card</label>
<input class="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="cardName" placeholder="ALEX MORGAN">
</div>
</div>
</section>
<button class="pay-btn w-full p-[13px] [background:linear-gradient(135deg,#6366f1,#8b5cf6)] text-[#fff] text-[15px] font-bold border-0 rounded-xl cursor-pointer [transition:all_.2s] flex items-center justify-center gap-2 font-[inherit] mt-1 hover:[transform:translateY(-2px)] hover:shadow-[0_8px_24px_rgba(99,102,241,.4)] active:[transform:scale(.98)]" id="payBtn" onclick="submitOrder()">
<span id="payBtnText">Pay $89.00</span>
<svg id="payBtnSpinner" class="[animation:spin_1s_linear_infinite] hidden" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10" stroke-opacity=".3"/><path d="M12 2a10 10 0 0 1 10 10"/></svg>
</button>
<p class="text-[11px] text-[#94a3b8] text-center mt-2.5">🔒 Payments secured by 256-bit SSL encryption.</p>
</div>
<!-- Right: Order summary -->
<div class="bg-[#fff] border rounded-2xl p-[22px] sticky top-5 max-[700px]:static max-[700px]:order-[-1]">
<h2 class="text-sm font-bold text-[#1e293b] mb-4">Order Summary</h2>
<div class="flex flex-col gap-3 mb-4">
<div class="flex items-center gap-2.5">
<div class="w-9 h-9 rounded-lg shrink-0" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)"></div>
<div class="flex-1 min-w-0">
<div class="text-xs font-semibold text-[#1e293b]">FWD Tools Pro Plan</div>
<div class="text-[11px] text-[#64748b]">12 months · Unlimited access</div>
</div>
<div class="text-[13px] font-bold text-[#1e293b] whitespace-nowrap">$79.00</div>
</div>
<div class="flex items-center gap-2.5">
<div class="w-9 h-9 rounded-lg shrink-0" style="background:linear-gradient(135deg,#10b981,#059669)"></div>
<div class="flex-1 min-w-0">
<div class="text-xs font-semibold text-[#1e293b]">Component Export Add-on</div>
<div class="text-[11px] text-[#64748b]">React + Vue + Angular</div>
</div>
<div class="text-[13px] font-bold text-[#1e293b] whitespace-nowrap">$10.00</div>
</div>
</div>
<div class="h-px bg-[#f1f5f9] mb-2.5"></div>
<div class="summary-row flex justify-between text-[13px] text-[#64748b] mb-1.5"><span>Subtotal</span><span>$89.00</span></div>
<div class="summary-row flex justify-between text-[13px] text-[#64748b] mb-1.5"><span>Discount (FIRST20)</span><span class="discount text-[#10b981] font-semibold">−$0.00</span></div>
<div class="summary-row flex justify-between text-[13px] text-[#64748b] mb-1.5"><span>Tax</span><span>$0.00</span></div>
<div class="summary-row flex justify-between text-[13px] text-[#64748b] mb-1.5 total"><span>Total</span><span>$89.00</span></div>
<div class="flex gap-1.5 mt-3.5">
<input type="text" class="flex-1 py-2 px-2.5 border rounded-lg text-xs text-[#1e293b] outline-none [transition:border-color_.15s] font-[inherit] focus:border-[#6366f1]" id="promoInput" placeholder="Promo code">
<button class="py-2 px-3.5 bg-[#1e293b] text-[#fff] border-0 rounded-lg text-xs font-bold cursor-pointer [transition:background_.15s] font-[inherit] whitespace-nowrap hover:bg-[#334155]" onclick="applyPromo()">Apply</button>
</div>
<div class="flex flex-col gap-[5px] mt-3.5 pt-3 border-t border-t-[#f1f5f9]">
<span class="text-[11px] text-[#10b981] font-semibold">✓ 30-day money back</span>
<span class="text-[11px] text-[#10b981] font-semibold">✓ Instant access</span>
<span class="text-[11px] text-[#10b981] font-semibold">✓ Cancel anytime</span>
</div>
</div>
</div>
</div>
<script>
function formatCard(input) {
let val = input.value.replace(/\D/g, '').slice(0, 16);
input.value = val.replace(/(\d{4})(?=\d)/g, '$1 ');
const indicator = document.getElementById('cardTypeIndicator');
if (val.startsWith('4')) indicator.textContent = 'VISA';
else if (val.startsWith('5')) indicator.textContent = 'MC';
else if (val.startsWith('3')) indicator.textContent = 'AMEX';
else indicator.textContent = '';
}
function formatExpiry(input) {
let val = input.value.replace(/\D/g, '').slice(0, 4);
if (val.length >= 2) val = val.slice(0, 2) + ' / ' + val.slice(2);
input.value = val;
}
function validate(input) {
const val = input.value.trim();
if (input.type === 'email') {
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
input.classList.toggle('valid', valid && val.length > 0);
input.classList.toggle('invalid', !valid && val.length > 0);
} else {
input.classList.toggle('valid', val.length >= 2);
input.classList.toggle('invalid', val.length === 1);
}
}
function applyPromo() {
const code = document.getElementById('promoInput').value.trim().toUpperCase();
if (code === 'FIRST20') {
document.querySelector('.discount').textContent = '−$17.80';
document.querySelector('.summary-row.total span:last-child').textContent = '$71.20';
document.getElementById('payBtn').querySelector('#payBtnText').textContent = 'Pay $71.20';
}
}
function submitOrder() {
const btn = document.getElementById('payBtn');
const text = document.getElementById('payBtnText');
const spinner = document.getElementById('payBtnSpinner');
btn.disabled = true;
text.textContent = 'Processing…';
spinner.classList.remove('hidden');
setTimeout(() => {
spinner.classList.add('hidden');
text.textContent = '✓ Order confirmed!';
btn.style.background = 'linear-gradient(135deg,#10b981,#059669)';
}, 2200);
}
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to CheckoutPaymentForm.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#f8fafc;min-height:100vh;padding:28px 16px}
.checkout-page{max-width:900px;margin:0 auto}
.checkout-wrap{display:grid;grid-template-columns:1fr 340px;gap:32px;align-items:start}
.checkout-title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:20px}
.form-section{margin-bottom:24px}
.section-label{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#94a3b8;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.section-label::after{content:'';flex:1;height:1px;background:#e2e8f0}
.field-row{display:flex;flex-direction:column;gap:12px;margin-bottom:12px}
.field-row.two-col{flex-direction:row}
.field-row.two-col .field{flex:1;min-width:0}
.field{display:flex;flex-direction:column;gap:4px}
.field-label{font-size:12px;font-weight:600;color:#374151}
.field-input{padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:13px;color:#1e293b;outline:none;transition:border-color .15s,box-shadow .15s;font-family:inherit;background:#fff}
.field-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.12)}
.field-input.valid{border-color:#10b981}
.field-input.invalid{border-color:#ef4444}
.field-error{font-size:11px;color:#ef4444;min-height:14px}
.card-icons{display:flex;align-items:center;gap:6px;margin-bottom:10px;flex-wrap:wrap}
.card-icon{font-size:9px;font-weight:800;padding:3px 7px;border-radius:4px}
.card-icon.visa{background:#1a1f71;color:#fff}
.card-icon.mc{background:#eb001b;color:#fff}
.card-icon.amex{background:#007bc1;color:#fff}
.card-icon.lock{color:#64748b;font-size:10px;background:#f1f5f9;border-radius:6px;padding:3px 8px}
.card-input-wrap{position:relative}
.card-num-input{width:100%;padding-right:60px}
.card-type-indicator{position:absolute;right:10px;top:50%;transform:translateY(-50%);font-size:10px;font-weight:700;color:#6366f1}
.pay-btn{width:100%;padding:13px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:15px;font-weight:700;border:none;border-radius:12px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;font-family:inherit;margin-top:4px}
.pay-btn:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(99,102,241,.4)}
.pay-btn:active{transform:scale(.98)}
.pay-btn:disabled{opacity:.7;cursor:not-allowed;transform:none}
.spinner{animation:spin 1s linear infinite}
@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
.hidden{display:none}
.secure-note{font-size:11px;color:#94a3b8;text-align:center;margin-top:10px}
/* Order summary */
.order-summary{background:#fff;border:1.5px solid #e2e8f0;border-radius:16px;padding:22px;position:sticky;top:20px}
.summary-title{font-size:14px;font-weight:700;color:#1e293b;margin-bottom:16px}
.summary-items{display:flex;flex-direction:column;gap:12px;margin-bottom:16px}
.summary-item{display:flex;align-items:center;gap:10px}
.item-thumb{width:36px;height:36px;border-radius:8px;flex-shrink:0}
.item-info{flex:1;min-width:0}
.item-name{font-size:12px;font-weight:600;color:#1e293b}
.item-desc{font-size:11px;color:#64748b}
.item-price{font-size:13px;font-weight:700;color:#1e293b;white-space:nowrap}
.summary-divider{height:1px;background:#f1f5f9;margin-bottom:10px}
.summary-row{display:flex;justify-content:space-between;font-size:13px;color:#64748b;margin-bottom:6px}
.summary-row.total{font-size:15px;font-weight:800;color:#1e293b;margin-top:6px;padding-top:6px;border-top:1.5px solid #e2e8f0}
.discount{color:#10b981;font-weight:600}
.promo-field{display:flex;gap:6px;margin-top:14px}
.promo-input{flex:1;padding:8px 10px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:12px;color:#1e293b;outline:none;transition:border-color .15s;font-family:inherit}
.promo-input:focus{border-color:#6366f1}
.promo-btn{padding:8px 14px;background:#1e293b;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer;transition:background .15s;font-family:inherit;white-space:nowrap}
.promo-btn:hover{background:#334155}
.trust-row{display:flex;flex-direction:column;gap:5px;margin-top:14px;padding-top:12px;border-top:1px solid #f1f5f9}
.trust-item{font-size:11px;color:#10b981;font-weight:600}
@media(max-width:700px){.checkout-wrap{grid-template-columns:1fr}.order-summary{position:static;order:-1}.field-row.two-col{flex-direction:column}}
`;
export default function CheckoutPaymentForm() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
function formatCard(input) {
let val = input.value.replace(/\D/g, '').slice(0, 16);
input.value = val.replace(/(\d{4})(?=\d)/g, '$1 ');
const indicator = document.getElementById('cardTypeIndicator');
if (val.startsWith('4')) indicator.textContent = 'VISA';
else if (val.startsWith('5')) indicator.textContent = 'MC';
else if (val.startsWith('3')) indicator.textContent = 'AMEX';
else indicator.textContent = '';
}
function formatExpiry(input) {
let val = input.value.replace(/\D/g, '').slice(0, 4);
if (val.length >= 2) val = val.slice(0, 2) + ' / ' + val.slice(2);
input.value = val;
}
function validate(input) {
const val = input.value.trim();
if (input.type === 'email') {
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
input.classList.toggle('valid', valid && val.length > 0);
input.classList.toggle('invalid', !valid && val.length > 0);
} else {
input.classList.toggle('valid', val.length >= 2);
input.classList.toggle('invalid', val.length === 1);
}
}
function applyPromo() {
const code = document.getElementById('promoInput').value.trim().toUpperCase();
if (code === 'FIRST20') {
document.querySelector('.discount').textContent = '−$17.80';
document.querySelector('.summary-row.total span:last-child').textContent = '$71.20';
document.getElementById('payBtn').querySelector('#payBtnText').textContent = 'Pay $71.20';
}
}
function submitOrder() {
const btn = document.getElementById('payBtn');
const text = document.getElementById('payBtnText');
const spinner = document.getElementById('payBtnSpinner');
btn.disabled = true;
text.textContent = 'Processing…';
spinner.classList.remove('hidden');
setTimeout(() => {
spinner.classList.add('hidden');
text.textContent = '✓ Order confirmed!';
btn.style.background = 'linear-gradient(135deg,#10b981,#059669)';
}, 2200);
}
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof validate === 'function') window.validate = validate;
if (typeof formatCard === 'function') window.formatCard = formatCard;
if (typeof formatExpiry === 'function') window.formatExpiry = formatExpiry;
if (typeof replace === 'function') window.replace = replace;
if (typeof submitOrder === 'function') window.submitOrder = submitOrder;
if (typeof applyPromo === 'function') window.applyPromo = applyPromo;
}, []);
return (
<>
<style>{css}</style>
<div className="checkout-page">
<div className="checkout-wrap">
<div className="checkout-form-col">
<h1 className="checkout-title">Checkout</h1>
<section className="form-section">
<h2 className="section-label">Contact</h2>
<div className="field-row">
<div className="field">
<label className="field-label" htmlFor="email">Email</label>
<input className="field-input" type="email" id="email" placeholder="[email protected]" onInput={(event) => { validate(event.currentTarget) }} />
<span className="field-error" id="emailError"></span>
</div>
</div>
</section>
<section className="form-section">
<h2 className="section-label">Delivery</h2>
<div className="field-row two-col">
<div className="field">
<label className="field-label" htmlFor="fname">First name</label>
<input className="field-input" type="text" id="fname" placeholder="Alex" onInput={(event) => { validate(event.currentTarget) }} />
</div>
<div className="field">
<label className="field-label" htmlFor="lname">Last name</label>
<input className="field-input" type="text" id="lname" placeholder="Morgan" onInput={(event) => { validate(event.currentTarget) }} />
</div>
</div>
<div className="field-row">
<div className="field">
<label className="field-label" htmlFor="address">Street address</label>
<input className="field-input" type="text" id="address" placeholder="123 Main St" onInput={(event) => { validate(event.currentTarget) }} />
</div>
</div>
<div className="field-row two-col">
<div className="field">
<label className="field-label" htmlFor="city">City</label>
<input className="field-input" type="text" id="city" placeholder="San Francisco" onInput={(event) => { validate(event.currentTarget) }} />
</div>
<div className="field">
<label className="field-label" htmlFor="zip">ZIP / Postcode</label>
<input className="field-input" type="text" id="zip" placeholder="94102" onInput={(event) => { validate(event.currentTarget) }} />
</div>
</div>
</section>
<section className="form-section">
<h2 className="section-label">Payment</h2>
<div className="card-icons">
<div className="card-icon visa">VISA</div>
<div className="card-icon mc">MC</div>
<div className="card-icon amex">AMEX</div>
<div className="card-icon lock">🔒 Secure</div>
</div>
<div className="field-row">
<div className="field">
<label className="field-label" htmlFor="cardNum">Card number</label>
<div className="card-input-wrap">
<input className="field-input card-num-input" type="text" id="cardNum" placeholder="1234 5678 9012 3456" maxlength="19" onInput={(event) => { formatCard(event.currentTarget) }} />
<div className="card-type-indicator" id="cardTypeIndicator"></div>
</div>
</div>
</div>
<div className="field-row two-col">
<div className="field">
<label className="field-label" htmlFor="expiry">Expiry</label>
<input className="field-input" type="text" id="expiry" placeholder="MM / YY" maxlength="7" onInput={(event) => { formatExpiry(event.currentTarget) }} />
</div>
<div className="field">
<label className="field-label" htmlFor="cvv">CVV</label>
<input className="field-input" type="text" id="cvv" placeholder="•••" maxlength="4" onInput={(event) => { event.currentTarget.value=event.currentTarget.value.replace(/\D/g,'') }} />
</div>
</div>
<div className="field-row">
<div className="field">
<label className="field-label" htmlFor="cardName">Name on card</label>
<input className="field-input" type="text" id="cardName" placeholder="ALEX MORGAN" />
</div>
</div>
</section>
<button className="pay-btn" id="payBtn" onClick={(event) => { submitOrder() }}>
<span id="payBtnText">Pay $89.00</span>
<svg id="payBtnSpinner" className="spinner hidden" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><circle cx="12" cy="12" r="10" stroke-opacity=".3"/><path d="M12 2a10 10 0 0 1 10 10"/></svg>
</button>
<p className="secure-note">🔒 Payments secured by 256-bit SSL encryption.</p>
</div>
<div className="order-summary">
<h2 className="summary-title">Order Summary</h2>
<div className="summary-items">
<div className="summary-item">
<div className="item-thumb" style={{ background: 'linear-gradient(135deg,#6366f1,#8b5cf6)' }}></div>
<div className="item-info">
<div className="item-name">FWD Tools Pro Plan</div>
<div className="item-desc">12 months · Unlimited access</div>
</div>
<div className="item-price">$79.00</div>
</div>
<div className="summary-item">
<div className="item-thumb" style={{ background: 'linear-gradient(135deg,#10b981,#059669)' }}></div>
<div className="item-info">
<div className="item-name">Component Export Add-on</div>
<div className="item-desc">React + Vue + Angular</div>
</div>
<div className="item-price">$10.00</div>
</div>
</div>
<div className="summary-divider"></div>
<div className="summary-row"><span>Subtotal</span><span>$89.00</span></div>
<div className="summary-row"><span>Discount (FIRST20)</span><span className="discount">−$0.00</span></div>
<div className="summary-row"><span>Tax</span><span>$0.00</span></div>
<div className="summary-row total"><span>Total</span><span>$89.00</span></div>
<div className="promo-field">
<input type="text" className="promo-input" id="promoInput" placeholder="Promo code" />
<button className="promo-btn" onClick={(event) => { applyPromo() }}>Apply</button>
</div>
<div className="trust-row">
<span className="trust-item">✓ 30-day money back</span>
<span className="trust-item">✓ Instant access</span>
<span className="trust-item">✓ Cancel anytime</span>
</div>
</div>
</div>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function CheckoutPaymentForm() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
function formatCard(input) {
let val = input.value.replace(/\D/g, '').slice(0, 16);
input.value = val.replace(/(\d{4})(?=\d)/g, '$1 ');
const indicator = document.getElementById('cardTypeIndicator');
if (val.startsWith('4')) indicator.textContent = 'VISA';
else if (val.startsWith('5')) indicator.textContent = 'MC';
else if (val.startsWith('3')) indicator.textContent = 'AMEX';
else indicator.textContent = '';
}
function formatExpiry(input) {
let val = input.value.replace(/\D/g, '').slice(0, 4);
if (val.length >= 2) val = val.slice(0, 2) + ' / ' + val.slice(2);
input.value = val;
}
function validate(input) {
const val = input.value.trim();
if (input.type === 'email') {
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
input.classList.toggle('valid', valid && val.length > 0);
input.classList.toggle('invalid', !valid && val.length > 0);
} else {
input.classList.toggle('valid', val.length >= 2);
input.classList.toggle('invalid', val.length === 1);
}
}
function applyPromo() {
const code = document.getElementById('promoInput').value.trim().toUpperCase();
if (code === 'FIRST20') {
document.querySelector('.discount').textContent = '−$17.80';
document.querySelector('.summary-row.total span:last-child').textContent = '$71.20';
document.getElementById('payBtn').querySelector('#payBtnText').textContent = 'Pay $71.20';
}
}
function submitOrder() {
const btn = document.getElementById('payBtn');
const text = document.getElementById('payBtnText');
const spinner = document.getElementById('payBtnSpinner');
btn.disabled = true;
text.textContent = 'Processing…';
spinner.classList.remove('hidden');
setTimeout(() => {
spinner.classList.add('hidden');
text.textContent = '✓ Order confirmed!';
btn.style.background = 'linear-gradient(135deg,#10b981,#059669)';
}, 2200);
}
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof validate === 'function') window.validate = validate;
if (typeof formatCard === 'function') window.formatCard = formatCard;
if (typeof formatExpiry === 'function') window.formatExpiry = formatExpiry;
if (typeof replace === 'function') window.replace = replace;
if (typeof submitOrder === 'function') window.submitOrder = submitOrder;
if (typeof applyPromo === 'function') window.applyPromo = applyPromo;
}, []);
return (
<>
<style>{`
@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,sans-serif;background:#f8fafc;min-height:100vh;padding:28px 16px
}
.section-label::after {
content:'';flex:1;height:1px;background:#e2e8f0
}
.field-row.two-col {
flex-direction:row
}
.field-row.two-col .field {
flex:1;min-width:0
}
.field-input.valid {
border-color:#10b981
}
.field-input.invalid {
border-color:#ef4444
}
.card-icon.visa {
background:#1a1f71;color:#fff
}
.card-icon.mc {
background:#eb001b;color:#fff
}
.card-icon.amex {
background:#007bc1;color:#fff
}
.card-icon.lock {
color:#64748b;font-size:10px;background:#f1f5f9;border-radius:6px;padding:3px 8px
}
.pay-btn:disabled {
opacity:.7;cursor:not-allowed;transform:none
}
.hidden {
display:none
}
.summary-row.total {
font-size:15px;font-weight:800;color:#1e293b;margin-top:6px;padding-top:6px;border-top:1.5px solid #e2e8f0
}
.field-row.two-col {
flex-direction:column
}
`}</style>
<div className="max-w-[900px] my-0 mx-auto">
<div className="grid grid-cols-1 gap-8 items-start max-[700px]:grid-cols-1">
<div className="checkout-form-col">
<h1 className="text-[22px] font-extrabold text-[#1e293b] mb-5">Checkout</h1>
<section className="mb-6">
<h2 className="section-label text-[11px] font-bold tracking-[.07em] uppercase text-[#94a3b8] mb-3 flex items-center gap-2">Contact</h2>
<div className="field-row flex flex-col gap-3 mb-3">
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="email">Email</label>
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="email" id="email" placeholder="[email protected]" onInput={(event) => { validate(event.currentTarget) }} />
<span className="text-[11px] text-[#ef4444] min-h-[14px]" id="emailError"></span>
</div>
</div>
</section>
<section className="mb-6">
<h2 className="section-label text-[11px] font-bold tracking-[.07em] uppercase text-[#94a3b8] mb-3 flex items-center gap-2">Delivery</h2>
<div className="field-row flex flex-col gap-3 mb-3 two-col">
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="fname">First name</label>
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="fname" placeholder="Alex" onInput={(event) => { validate(event.currentTarget) }} />
</div>
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="lname">Last name</label>
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="lname" placeholder="Morgan" onInput={(event) => { validate(event.currentTarget) }} />
</div>
</div>
<div className="field-row flex flex-col gap-3 mb-3">
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="address">Street address</label>
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="address" placeholder="123 Main St" onInput={(event) => { validate(event.currentTarget) }} />
</div>
</div>
<div className="field-row flex flex-col gap-3 mb-3 two-col">
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="city">City</label>
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="city" placeholder="San Francisco" onInput={(event) => { validate(event.currentTarget) }} />
</div>
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="zip">ZIP / Postcode</label>
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="zip" placeholder="94102" onInput={(event) => { validate(event.currentTarget) }} />
</div>
</div>
</section>
<section className="mb-6">
<h2 className="section-label text-[11px] font-bold tracking-[.07em] uppercase text-[#94a3b8] mb-3 flex items-center gap-2">Payment</h2>
<div className="flex items-center gap-1.5 mb-2.5 flex-wrap">
<div className="card-icon text-[9px] font-extrabold py-[3px] px-[7px] rounded visa">VISA</div>
<div className="card-icon text-[9px] font-extrabold py-[3px] px-[7px] rounded mc">MC</div>
<div className="card-icon text-[9px] font-extrabold py-[3px] px-[7px] rounded amex">AMEX</div>
<div className="card-icon text-[9px] font-extrabold py-[3px] px-[7px] rounded lock">🔒 Secure</div>
</div>
<div className="field-row flex flex-col gap-3 mb-3">
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="cardNum">Card number</label>
<div className="relative">
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)] w-full pr-[60px]" type="text" id="cardNum" placeholder="1234 5678 9012 3456" maxlength="19" onInput={(event) => { formatCard(event.currentTarget) }} />
<div className="absolute right-2.5 top-1/2 [transform:translateY(-50%)] text-xs font-bold text-[#6366f1]" id="cardTypeIndicator"></div>
</div>
</div>
</div>
<div className="field-row flex flex-col gap-3 mb-3 two-col">
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="expiry">Expiry</label>
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="expiry" placeholder="MM / YY" maxlength="7" onInput={(event) => { formatExpiry(event.currentTarget) }} />
</div>
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="cvv">CVV</label>
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="cvv" placeholder="•••" maxlength="4" onInput={(event) => { event.currentTarget.value=event.currentTarget.value.replace(/\D/g,'') }} />
</div>
</div>
<div className="field-row flex flex-col gap-3 mb-3">
<div className="field flex flex-col gap-1">
<label className="text-xs font-semibold text-[#374151]" htmlFor="cardName">Name on card</label>
<input className="field-input py-[9px] px-3 border rounded-[10px] text-[13px] text-[#1e293b] outline-none [transition:border-color_.15s,box-shadow_.15s] font-[inherit] bg-[#fff] focus:border-[#6366f1] focus:shadow-[0_0_0_3px_rgba(99,102,241,.12)]" type="text" id="cardName" placeholder="ALEX MORGAN" />
</div>
</div>
</section>
<button className="pay-btn w-full p-[13px] [background:linear-gradient(135deg,#6366f1,#8b5cf6)] text-[#fff] text-[15px] font-bold border-0 rounded-xl cursor-pointer [transition:all_.2s] flex items-center justify-center gap-2 font-[inherit] mt-1 hover:[transform:translateY(-2px)] hover:shadow-[0_8px_24px_rgba(99,102,241,.4)] active:[transform:scale(.98)]" id="payBtn" onClick={(event) => { submitOrder() }}>
<span id="payBtnText">Pay $89.00</span>
<svg id="payBtnSpinner" className="[animation:spin_1s_linear_infinite] hidden" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><circle cx="12" cy="12" r="10" stroke-opacity=".3"/><path d="M12 2a10 10 0 0 1 10 10"/></svg>
</button>
<p className="text-[11px] text-[#94a3b8] text-center mt-2.5">🔒 Payments secured by 256-bit SSL encryption.</p>
</div>
<div className="bg-[#fff] border rounded-2xl p-[22px] sticky top-5 max-[700px]:static max-[700px]:order-[-1]">
<h2 className="text-sm font-bold text-[#1e293b] mb-4">Order Summary</h2>
<div className="flex flex-col gap-3 mb-4">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-lg shrink-0" style={{ background: 'linear-gradient(135deg,#6366f1,#8b5cf6)' }}></div>
<div className="flex-1 min-w-0">
<div className="text-xs font-semibold text-[#1e293b]">FWD Tools Pro Plan</div>
<div className="text-[11px] text-[#64748b]">12 months · Unlimited access</div>
</div>
<div className="text-[13px] font-bold text-[#1e293b] whitespace-nowrap">$79.00</div>
</div>
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-lg shrink-0" style={{ background: 'linear-gradient(135deg,#10b981,#059669)' }}></div>
<div className="flex-1 min-w-0">
<div className="text-xs font-semibold text-[#1e293b]">Component Export Add-on</div>
<div className="text-[11px] text-[#64748b]">React + Vue + Angular</div>
</div>
<div className="text-[13px] font-bold text-[#1e293b] whitespace-nowrap">$10.00</div>
</div>
</div>
<div className="h-px bg-[#f1f5f9] mb-2.5"></div>
<div className="summary-row flex justify-between text-[13px] text-[#64748b] mb-1.5"><span>Subtotal</span><span>$89.00</span></div>
<div className="summary-row flex justify-between text-[13px] text-[#64748b] mb-1.5"><span>Discount (FIRST20)</span><span className="discount text-[#10b981] font-semibold">−$0.00</span></div>
<div className="summary-row flex justify-between text-[13px] text-[#64748b] mb-1.5"><span>Tax</span><span>$0.00</span></div>
<div className="summary-row flex justify-between text-[13px] text-[#64748b] mb-1.5 total"><span>Total</span><span>$89.00</span></div>
<div className="flex gap-1.5 mt-3.5">
<input type="text" className="flex-1 py-2 px-2.5 border rounded-lg text-xs text-[#1e293b] outline-none [transition:border-color_.15s] font-[inherit] focus:border-[#6366f1]" id="promoInput" placeholder="Promo code" />
<button className="py-2 px-3.5 bg-[#1e293b] text-[#fff] border-0 rounded-lg text-xs font-bold cursor-pointer [transition:background_.15s] font-[inherit] whitespace-nowrap hover:bg-[#334155]" onClick={(event) => { applyPromo() }}>Apply</button>
</div>
<div className="flex flex-col gap-[5px] mt-3.5 pt-3 border-t border-t-[#f1f5f9]">
<span className="text-[11px] text-[#10b981] font-semibold">✓ 30-day money back</span>
<span className="text-[11px] text-[#10b981] font-semibold">✓ Instant access</span>
<span className="text-[11px] text-[#10b981] font-semibold">✓ Cancel anytime</span>
</div>
</div>
</div>
</div>
</>
);
}<template>
<div class="checkout-page">
<div class="checkout-wrap">
<!-- Left: Form -->
<div class="checkout-form-col">
<h1 class="checkout-title">Checkout</h1>
<section class="form-section">
<h2 class="section-label">Contact</h2>
<div class="field-row">
<div class="field">
<label class="field-label" for="email">Email</label>
<input class="field-input" type="email" id="email" placeholder="[email protected]" @input="validate($event.currentTarget)">
<span class="field-error" id="emailError"></span>
</div>
</div>
</section>
<section class="form-section">
<h2 class="section-label">Delivery</h2>
<div class="field-row two-col">
<div class="field">
<label class="field-label" for="fname">First name</label>
<input class="field-input" type="text" id="fname" placeholder="Alex" @input="validate($event.currentTarget)">
</div>
<div class="field">
<label class="field-label" for="lname">Last name</label>
<input class="field-input" type="text" id="lname" placeholder="Morgan" @input="validate($event.currentTarget)">
</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label" for="address">Street address</label>
<input class="field-input" type="text" id="address" placeholder="123 Main St" @input="validate($event.currentTarget)">
</div>
</div>
<div class="field-row two-col">
<div class="field">
<label class="field-label" for="city">City</label>
<input class="field-input" type="text" id="city" placeholder="San Francisco" @input="validate($event.currentTarget)">
</div>
<div class="field">
<label class="field-label" for="zip">ZIP / Postcode</label>
<input class="field-input" type="text" id="zip" placeholder="94102" @input="validate($event.currentTarget)">
</div>
</div>
</section>
<section class="form-section">
<h2 class="section-label">Payment</h2>
<div class="card-icons">
<div class="card-icon visa">VISA</div>
<div class="card-icon mc">MC</div>
<div class="card-icon amex">AMEX</div>
<div class="card-icon lock">🔒 Secure</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label" for="cardNum">Card number</label>
<div class="card-input-wrap">
<input class="field-input card-num-input" type="text" id="cardNum" placeholder="1234 5678 9012 3456" maxlength="19" @input="formatCard($event.currentTarget)">
<div class="card-type-indicator" id="cardTypeIndicator"></div>
</div>
</div>
</div>
<div class="field-row two-col">
<div class="field">
<label class="field-label" for="expiry">Expiry</label>
<input class="field-input" type="text" id="expiry" placeholder="MM / YY" maxlength="7" @input="formatExpiry($event.currentTarget)">
</div>
<div class="field">
<label class="field-label" for="cvv">CVV</label>
<input class="field-input" type="text" id="cvv" placeholder="•••" maxlength="4" @input="$event.currentTarget.value=$event.currentTarget.value.replace(/\D/g,'')">
</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label" for="cardName">Name on card</label>
<input class="field-input" type="text" id="cardName" placeholder="ALEX MORGAN">
</div>
</div>
</section>
<button class="pay-btn" id="payBtn" @click="submitOrder()">
<span id="payBtnText">Pay $89.00</span>
<svg id="payBtnSpinner" class="spinner hidden" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10" stroke-opacity=".3"/><path d="M12 2a10 10 0 0 1 10 10"/></svg>
</button>
<p class="secure-note">🔒 Payments secured by 256-bit SSL encryption.</p>
</div>
<!-- Right: Order summary -->
<div class="order-summary">
<h2 class="summary-title">Order Summary</h2>
<div class="summary-items">
<div class="summary-item">
<div class="item-thumb" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)"></div>
<div class="item-info">
<div class="item-name">FWD Tools Pro Plan</div>
<div class="item-desc">12 months · Unlimited access</div>
</div>
<div class="item-price">$79.00</div>
</div>
<div class="summary-item">
<div class="item-thumb" style="background:linear-gradient(135deg,#10b981,#059669)"></div>
<div class="item-info">
<div class="item-name">Component Export Add-on</div>
<div class="item-desc">React + Vue + Angular</div>
</div>
<div class="item-price">$10.00</div>
</div>
</div>
<div class="summary-divider"></div>
<div class="summary-row"><span>Subtotal</span><span>$89.00</span></div>
<div class="summary-row"><span>Discount (FIRST20)</span><span class="discount">−$0.00</span></div>
<div class="summary-row"><span>Tax</span><span>$0.00</span></div>
<div class="summary-row total"><span>Total</span><span>$89.00</span></div>
<div class="promo-field">
<input type="text" class="promo-input" id="promoInput" placeholder="Promo code">
<button class="promo-btn" @click="applyPromo()">Apply</button>
</div>
<div class="trust-row">
<span class="trust-item">✓ 30-day money back</span>
<span class="trust-item">✓ Instant access</span>
<span class="trust-item">✓ Cancel anytime</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
function formatCard(input) {
let val = input.value.replace(/\D/g, '').slice(0, 16);
input.value = val.replace(/(\d{4})(?=\d)/g, '$1 ');
const indicator = document.getElementById('cardTypeIndicator');
if (val.startsWith('4')) indicator.textContent = 'VISA';
else if (val.startsWith('5')) indicator.textContent = 'MC';
else if (val.startsWith('3')) indicator.textContent = 'AMEX';
else indicator.textContent = '';
}
function formatExpiry(input) {
let val = input.value.replace(/\D/g, '').slice(0, 4);
if (val.length >= 2) val = val.slice(0, 2) + ' / ' + val.slice(2);
input.value = val;
}
function validate(input) {
const val = input.value.trim();
if (input.type === 'email') {
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
input.classList.toggle('valid', valid && val.length > 0);
input.classList.toggle('invalid', !valid && val.length > 0);
} else {
input.classList.toggle('valid', val.length >= 2);
input.classList.toggle('invalid', val.length === 1);
}
}
function applyPromo() {
const code = document.getElementById('promoInput').value.trim().toUpperCase();
if (code === 'FIRST20') {
document.querySelector('.discount').textContent = '−$17.80';
document.querySelector('.summary-row.total span:last-child').textContent = '$71.20';
document.getElementById('payBtn').querySelector('#payBtnText').textContent = 'Pay $71.20';
}
}
function submitOrder() {
const btn = document.getElementById('payBtn');
const text = document.getElementById('payBtnText');
const spinner = document.getElementById('payBtnSpinner');
btn.disabled = true;
text.textContent = 'Processing…';
spinner.classList.remove('hidden');
setTimeout(() => {
spinner.classList.add('hidden');
text.textContent = '✓ Order confirmed!';
btn.style.background = 'linear-gradient(135deg,#10b981,#059669)';
}, 2200);
}
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#f8fafc;min-height:100vh;padding:28px 16px}
.checkout-page{max-width:900px;margin:0 auto}
.checkout-wrap{display:grid;grid-template-columns:1fr 340px;gap:32px;align-items:start}
.checkout-title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:20px}
.form-section{margin-bottom:24px}
.section-label{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#94a3b8;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.section-label::after{content:'';flex:1;height:1px;background:#e2e8f0}
.field-row{display:flex;flex-direction:column;gap:12px;margin-bottom:12px}
.field-row.two-col{flex-direction:row}
.field-row.two-col .field{flex:1;min-width:0}
.field{display:flex;flex-direction:column;gap:4px}
.field-label{font-size:12px;font-weight:600;color:#374151}
.field-input{padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:13px;color:#1e293b;outline:none;transition:border-color .15s,box-shadow .15s;font-family:inherit;background:#fff}
.field-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.12)}
.field-input.valid{border-color:#10b981}
.field-input.invalid{border-color:#ef4444}
.field-error{font-size:11px;color:#ef4444;min-height:14px}
.card-icons{display:flex;align-items:center;gap:6px;margin-bottom:10px;flex-wrap:wrap}
.card-icon{font-size:9px;font-weight:800;padding:3px 7px;border-radius:4px}
.card-icon.visa{background:#1a1f71;color:#fff}
.card-icon.mc{background:#eb001b;color:#fff}
.card-icon.amex{background:#007bc1;color:#fff}
.card-icon.lock{color:#64748b;font-size:10px;background:#f1f5f9;border-radius:6px;padding:3px 8px}
.card-input-wrap{position:relative}
.card-num-input{width:100%;padding-right:60px}
.card-type-indicator{position:absolute;right:10px;top:50%;transform:translateY(-50%);font-size:10px;font-weight:700;color:#6366f1}
.pay-btn{width:100%;padding:13px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:15px;font-weight:700;border:none;border-radius:12px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;font-family:inherit;margin-top:4px}
.pay-btn:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(99,102,241,.4)}
.pay-btn:active{transform:scale(.98)}
.pay-btn:disabled{opacity:.7;cursor:not-allowed;transform:none}
.spinner{animation:spin 1s linear infinite}
@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
.hidden{display:none}
.secure-note{font-size:11px;color:#94a3b8;text-align:center;margin-top:10px}
/* Order summary */
.order-summary{background:#fff;border:1.5px solid #e2e8f0;border-radius:16px;padding:22px;position:sticky;top:20px}
.summary-title{font-size:14px;font-weight:700;color:#1e293b;margin-bottom:16px}
.summary-items{display:flex;flex-direction:column;gap:12px;margin-bottom:16px}
.summary-item{display:flex;align-items:center;gap:10px}
.item-thumb{width:36px;height:36px;border-radius:8px;flex-shrink:0}
.item-info{flex:1;min-width:0}
.item-name{font-size:12px;font-weight:600;color:#1e293b}
.item-desc{font-size:11px;color:#64748b}
.item-price{font-size:13px;font-weight:700;color:#1e293b;white-space:nowrap}
.summary-divider{height:1px;background:#f1f5f9;margin-bottom:10px}
.summary-row{display:flex;justify-content:space-between;font-size:13px;color:#64748b;margin-bottom:6px}
.summary-row.total{font-size:15px;font-weight:800;color:#1e293b;margin-top:6px;padding-top:6px;border-top:1.5px solid #e2e8f0}
.discount{color:#10b981;font-weight:600}
.promo-field{display:flex;gap:6px;margin-top:14px}
.promo-input{flex:1;padding:8px 10px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:12px;color:#1e293b;outline:none;transition:border-color .15s;font-family:inherit}
.promo-input:focus{border-color:#6366f1}
.promo-btn{padding:8px 14px;background:#1e293b;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer;transition:background .15s;font-family:inherit;white-space:nowrap}
.promo-btn:hover{background:#334155}
.trust-row{display:flex;flex-direction:column;gap:5px;margin-top:14px;padding-top:12px;border-top:1px solid #f1f5f9}
.trust-item{font-size:11px;color:#10b981;font-weight:600}
@media(max-width:700px){.checkout-wrap{grid-template-columns:1fr}.order-summary{position:static;order:-1}.field-row.two-col{flex-direction:column}}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-checkout-payment-form',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="checkout-page">
<div class="checkout-wrap">
<!-- Left: Form -->
<div class="checkout-form-col">
<h1 class="checkout-title">Checkout</h1>
<section class="form-section">
<h2 class="section-label">Contact</h2>
<div class="field-row">
<div class="field">
<label class="field-label" for="email">Email</label>
<input class="field-input" type="email" id="email" placeholder="[email protected]" (input)="validate($event.currentTarget)">
<span class="field-error" id="emailError"></span>
</div>
</div>
</section>
<section class="form-section">
<h2 class="section-label">Delivery</h2>
<div class="field-row two-col">
<div class="field">
<label class="field-label" for="fname">First name</label>
<input class="field-input" type="text" id="fname" placeholder="Alex" (input)="validate($event.currentTarget)">
</div>
<div class="field">
<label class="field-label" for="lname">Last name</label>
<input class="field-input" type="text" id="lname" placeholder="Morgan" (input)="validate($event.currentTarget)">
</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label" for="address">Street address</label>
<input class="field-input" type="text" id="address" placeholder="123 Main St" (input)="validate($event.currentTarget)">
</div>
</div>
<div class="field-row two-col">
<div class="field">
<label class="field-label" for="city">City</label>
<input class="field-input" type="text" id="city" placeholder="San Francisco" (input)="validate($event.currentTarget)">
</div>
<div class="field">
<label class="field-label" for="zip">ZIP / Postcode</label>
<input class="field-input" type="text" id="zip" placeholder="94102" (input)="validate($event.currentTarget)">
</div>
</div>
</section>
<section class="form-section">
<h2 class="section-label">Payment</h2>
<div class="card-icons">
<div class="card-icon visa">VISA</div>
<div class="card-icon mc">MC</div>
<div class="card-icon amex">AMEX</div>
<div class="card-icon lock">🔒 Secure</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label" for="cardNum">Card number</label>
<div class="card-input-wrap">
<input class="field-input card-num-input" type="text" id="cardNum" placeholder="1234 5678 9012 3456" maxlength="19" (input)="formatCard($event.currentTarget)">
<div class="card-type-indicator" id="cardTypeIndicator"></div>
</div>
</div>
</div>
<div class="field-row two-col">
<div class="field">
<label class="field-label" for="expiry">Expiry</label>
<input class="field-input" type="text" id="expiry" placeholder="MM / YY" maxlength="7" (input)="formatExpiry($event.currentTarget)">
</div>
<div class="field">
<label class="field-label" for="cvv">CVV</label>
<input class="field-input" type="text" id="cvv" placeholder="•••" maxlength="4" (input)="$event.currentTarget.value=$event.currentTarget.value.replace(/\D/g,'')">
</div>
</div>
<div class="field-row">
<div class="field">
<label class="field-label" for="cardName">Name on card</label>
<input class="field-input" type="text" id="cardName" placeholder="ALEX MORGAN">
</div>
</div>
</section>
<button class="pay-btn" id="payBtn" (click)="submitOrder()">
<span id="payBtnText">Pay $89.00</span>
<svg id="payBtnSpinner" class="spinner hidden" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10" stroke-opacity=".3"/><path d="M12 2a10 10 0 0 1 10 10"/></svg>
</button>
<p class="secure-note">🔒 Payments secured by 256-bit SSL encryption.</p>
</div>
<!-- Right: Order summary -->
<div class="order-summary">
<h2 class="summary-title">Order Summary</h2>
<div class="summary-items">
<div class="summary-item">
<div class="item-thumb" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)"></div>
<div class="item-info">
<div class="item-name">FWD Tools Pro Plan</div>
<div class="item-desc">12 months · Unlimited access</div>
</div>
<div class="item-price">$79.00</div>
</div>
<div class="summary-item">
<div class="item-thumb" style="background:linear-gradient(135deg,#10b981,#059669)"></div>
<div class="item-info">
<div class="item-name">Component Export Add-on</div>
<div class="item-desc">React + Vue + Angular</div>
</div>
<div class="item-price">$10.00</div>
</div>
</div>
<div class="summary-divider"></div>
<div class="summary-row"><span>Subtotal</span><span>$89.00</span></div>
<div class="summary-row"><span>Discount (FIRST20)</span><span class="discount">−$0.00</span></div>
<div class="summary-row"><span>Tax</span><span>$0.00</span></div>
<div class="summary-row total"><span>Total</span><span>$89.00</span></div>
<div class="promo-field">
<input type="text" class="promo-input" id="promoInput" placeholder="Promo code">
<button class="promo-btn" (click)="applyPromo()">Apply</button>
</div>
<div class="trust-row">
<span class="trust-item">✓ 30-day money back</span>
<span class="trust-item">✓ Instant access</span>
<span class="trust-item">✓ Cancel anytime</span>
</div>
</div>
</div>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#f8fafc;min-height:100vh;padding:28px 16px}
.checkout-page{max-width:900px;margin:0 auto}
.checkout-wrap{display:grid;grid-template-columns:1fr 340px;gap:32px;align-items:start}
.checkout-title{font-size:22px;font-weight:800;color:#1e293b;margin-bottom:20px}
.form-section{margin-bottom:24px}
.section-label{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:#94a3b8;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.section-label::after{content:'';flex:1;height:1px;background:#e2e8f0}
.field-row{display:flex;flex-direction:column;gap:12px;margin-bottom:12px}
.field-row.two-col{flex-direction:row}
.field-row.two-col .field{flex:1;min-width:0}
.field{display:flex;flex-direction:column;gap:4px}
.field-label{font-size:12px;font-weight:600;color:#374151}
.field-input{padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:10px;font-size:13px;color:#1e293b;outline:none;transition:border-color .15s,box-shadow .15s;font-family:inherit;background:#fff}
.field-input:focus{border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.12)}
.field-input.valid{border-color:#10b981}
.field-input.invalid{border-color:#ef4444}
.field-error{font-size:11px;color:#ef4444;min-height:14px}
.card-icons{display:flex;align-items:center;gap:6px;margin-bottom:10px;flex-wrap:wrap}
.card-icon{font-size:9px;font-weight:800;padding:3px 7px;border-radius:4px}
.card-icon.visa{background:#1a1f71;color:#fff}
.card-icon.mc{background:#eb001b;color:#fff}
.card-icon.amex{background:#007bc1;color:#fff}
.card-icon.lock{color:#64748b;font-size:10px;background:#f1f5f9;border-radius:6px;padding:3px 8px}
.card-input-wrap{position:relative}
.card-num-input{width:100%;padding-right:60px}
.card-type-indicator{position:absolute;right:10px;top:50%;transform:translateY(-50%);font-size:10px;font-weight:700;color:#6366f1}
.pay-btn{width:100%;padding:13px;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;font-size:15px;font-weight:700;border:none;border-radius:12px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;font-family:inherit;margin-top:4px}
.pay-btn:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(99,102,241,.4)}
.pay-btn:active{transform:scale(.98)}
.pay-btn:disabled{opacity:.7;cursor:not-allowed;transform:none}
.spinner{animation:spin 1s linear infinite}
@keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
.hidden{display:none}
.secure-note{font-size:11px;color:#94a3b8;text-align:center;margin-top:10px}
/* Order summary */
.order-summary{background:#fff;border:1.5px solid #e2e8f0;border-radius:16px;padding:22px;position:sticky;top:20px}
.summary-title{font-size:14px;font-weight:700;color:#1e293b;margin-bottom:16px}
.summary-items{display:flex;flex-direction:column;gap:12px;margin-bottom:16px}
.summary-item{display:flex;align-items:center;gap:10px}
.item-thumb{width:36px;height:36px;border-radius:8px;flex-shrink:0}
.item-info{flex:1;min-width:0}
.item-name{font-size:12px;font-weight:600;color:#1e293b}
.item-desc{font-size:11px;color:#64748b}
.item-price{font-size:13px;font-weight:700;color:#1e293b;white-space:nowrap}
.summary-divider{height:1px;background:#f1f5f9;margin-bottom:10px}
.summary-row{display:flex;justify-content:space-between;font-size:13px;color:#64748b;margin-bottom:6px}
.summary-row.total{font-size:15px;font-weight:800;color:#1e293b;margin-top:6px;padding-top:6px;border-top:1.5px solid #e2e8f0}
.discount{color:#10b981;font-weight:600}
.promo-field{display:flex;gap:6px;margin-top:14px}
.promo-input{flex:1;padding:8px 10px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:12px;color:#1e293b;outline:none;transition:border-color .15s;font-family:inherit}
.promo-input:focus{border-color:#6366f1}
.promo-btn{padding:8px 14px;background:#1e293b;color:#fff;border:none;border-radius:8px;font-size:12px;font-weight:700;cursor:pointer;transition:background .15s;font-family:inherit;white-space:nowrap}
.promo-btn:hover{background:#334155}
.trust-row{display:flex;flex-direction:column;gap:5px;margin-top:14px;padding-top:12px;border-top:1px solid #f1f5f9}
.trust-item{font-size:11px;color:#10b981;font-weight:600}
@media(max-width:700px){.checkout-wrap{grid-template-columns:1fr}.order-summary{position:static;order:-1}.field-row.two-col{flex-direction:column}}
`]
})
export class CheckoutPaymentFormComponent implements AfterViewInit {
ngAfterViewInit(): void {
function formatCard(input) {
let val = input.value.replace(/\D/g, '').slice(0, 16);
input.value = val.replace(/(\d{4})(?=\d)/g, '$1 ');
const indicator = document.getElementById('cardTypeIndicator');
if (val.startsWith('4')) indicator.textContent = 'VISA';
else if (val.startsWith('5')) indicator.textContent = 'MC';
else if (val.startsWith('3')) indicator.textContent = 'AMEX';
else indicator.textContent = '';
}
function formatExpiry(input) {
let val = input.value.replace(/\D/g, '').slice(0, 4);
if (val.length >= 2) val = val.slice(0, 2) + ' / ' + val.slice(2);
input.value = val;
}
function validate(input) {
const val = input.value.trim();
if (input.type === 'email') {
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
input.classList.toggle('valid', valid && val.length > 0);
input.classList.toggle('invalid', !valid && val.length > 0);
} else {
input.classList.toggle('valid', val.length >= 2);
input.classList.toggle('invalid', val.length === 1);
}
}
function applyPromo() {
const code = document.getElementById('promoInput').value.trim().toUpperCase();
if (code === 'FIRST20') {
document.querySelector('.discount').textContent = '−$17.80';
document.querySelector('.summary-row.total span:last-child').textContent = '$71.20';
document.getElementById('payBtn').querySelector('#payBtnText').textContent = 'Pay $71.20';
}
}
function submitOrder() {
const btn = document.getElementById('payBtn');
const text = document.getElementById('payBtnText');
const spinner = document.getElementById('payBtnSpinner');
btn.disabled = true;
text.textContent = 'Processing…';
spinner.classList.remove('hidden');
setTimeout(() => {
spinner.classList.add('hidden');
text.textContent = '✓ Order confirmed!';
btn.style.background = 'linear-gradient(135deg,#10b981,#059669)';
}, 2200);
}
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { formatCard, formatExpiry, validate, applyPromo, submitOrder });
}
}