diff --git a/resources/css/checkout.css b/resources/css/checkout.css deleted file mode 100644 index 7f77fd3..0000000 --- a/resources/css/checkout.css +++ /dev/null @@ -1,991 +0,0 @@ -/* - * Cart + checkout module — generic default styling. - * - * Deliberately NOT wrapped in a Tailwind-style `@layer`. An earlier version - * put these rules in `@layer bbk-checkout`, positioned (via a cross-file - * @layer ordering statement) to sit between Tailwind's `base` and - * `components` — in theory enough to beat Preflight's element resets while - * still losing to a host override. In practice a build tool processing each - * CSS file in isolation (Vite/Lightning CSS here) optimizes away exactly the - * cross-file ordering information that trick depends on, so it silently - * didn't work: Preflight's `button { background-color: transparent }`, - * `* { border-width: 0 }` etc. (layered, in `base`) were beating every - * `.bbk-*` rule below regardless of specificity — buttons with no - * background, no border, wrong font-size. - * - * Plain, unlayered CSS sidesteps the whole problem: an unlayered rule always - * beats ANY layered rule (Preflight included), full stop, no ordering tricks, - * nothing a bundler can silently invalidate. This file is loaded BEFORE the - * host's own stylesheet (see the @vite call in the layout ), so: - * - * - a later PLAIN (unlayered) `.bbk-*` rule in the host stylesheet wins — - * same specificity, later in source order - * - a later host rule with a MORE specific selector wins regardless - * - a host rule inside `@layer components`/`@layer utilities` does NOT - * win — unlayered always beats layered. Theme this module from plain - * rules in app.css, not from inside a Tailwind layer. - * - * Two ways to theme this, cheapest first: - * - * 1. Redefine the --bbk-* custom properties below (from :root, or scoped to - * .bbk-cart for a cart-only override) — covers colour, radius, shadow, - * font without touching a single selector below. - * - * :root { --bbk-color-accent: var(--color-brand); --bbk-radius: 0; } - * - * 2. Override individual `.bbk-*` rules directly (as plain rules, per - * above) for anything structural (spacing, layout) the variables don't - * cover. - * - * This file's own look is a deliberately neutral placeholder — inoffensive, - * not "designed" — so a project always has something reasonable before it - * themes; it is not meant to be edited per project. - */ - -:root { - --bbk-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - - --bbk-color-text: #18181b; - --bbk-color-muted: #71717a; - --bbk-color-bg: #ffffff; - --bbk-color-bg-muted: #f4f4f5; - --bbk-color-border: #e4e4e7; - --bbk-color-accent: #18181b; - --bbk-color-accent-text: #ffffff; - --bbk-color-danger: #dc2626; - - --bbk-radius: 8px; - --bbk-radius-sm: 4px; - --bbk-shadow: 0 12px 32px rgba(0, 0, 0, 0.16); -} - -.bbk-cart[hidden] { display: none; } - -.bbk-cart { - position: fixed; - inset: 0; - z-index: 1000; - font-family: var(--bbk-font); - font-size: 0.9375rem; - line-height: 1.4; - color: var(--bbk-color-text); -} - -.bbk-cart-backdrop { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.4); - opacity: 0; - transition: opacity 0.25s ease; -} - -.bbk-cart[data-bbk-cart-state="open"] .bbk-cart-backdrop { opacity: 1; } - -.bbk-cart-panel { - position: absolute; - top: 0; - right: 0; - display: flex; - flex-direction: column; - width: min(420px, 100vw); - height: 100%; - background: var(--bbk-color-bg); - box-shadow: var(--bbk-shadow); - transform: translateX(100%); - transition: transform 0.25s ease; -} - -.bbk-cart[data-bbk-cart-state="open"] .bbk-cart-panel { transform: translateX(0); } - -.bbk-cart-panel-header { - flex: 0 0 auto; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1.5rem 1.5rem 1.25rem; - border-bottom: 1px solid var(--bbk-color-border); -} - -.bbk-cart-heading { - margin: 0; - font-size: 1.375rem; - font-weight: 700; -} - -.bbk-cart-dismiss, -.bbk-cart-item-remove, -.bbk-cart-qty-btn { - cursor: pointer; - background: none; - border: 0; - padding: 0; - font: inherit; - line-height: 1; - color: var(--bbk-color-muted); - transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; -} - -.bbk-cart-dismiss { - font-size: 1.75rem; - width: 2.5rem; - height: 2.5rem; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: var(--bbk-radius-sm); - flex-shrink: 0; -} - -.bbk-cart-dismiss:hover { color: var(--bbk-color-text); background: var(--bbk-color-bg-muted); } - -.bbk-cart-item-remove:hover { color: var(--bbk-color-danger); } - -.bbk-cart-dismiss:focus-visible, -.bbk-cart-item-remove:focus-visible, -.bbk-cart-qty-btn:focus-visible, -.bbk-cart-qty-input:focus-visible, -.bbk-cart-checkout:focus-visible, -.bbk-cart-coupon-input:focus-visible, -.bbk-cart-coupon-submit:focus-visible, -.bbk-cart-coupon-remove:focus-visible { - outline: 2px solid var(--bbk-color-accent); - outline-offset: 2px; -} - -.bbk-visually-hidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -.bbk-cart-panel-body { - flex: 1 1 auto; - overflow-y: auto; - overscroll-behavior: contain; - padding: 1.5rem; -} - -.bbk-cart-items { - list-style: none; - margin: 0 0 2rem; - padding: 0; - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.bbk-cart-item { - display: grid; - grid-template-columns: 72px 1fr auto; - gap: 0.875rem; - align-items: start; -} - -.bbk-cart-item-media img { - display: block; - width: 72px; - height: 72px; - object-fit: cover; - border-radius: var(--bbk-radius-sm); - background: var(--bbk-color-bg-muted); -} - -.bbk-cart-item-detail { min-width: 0; } - -.bbk-cart-item-title { - display: block; - margin: 0 0 0.25rem; - font-weight: 600; - color: inherit; - text-decoration: none; -} - -a.bbk-cart-item-title:hover { text-decoration: underline; } - -.bbk-cart-item-variant { - margin: 0 0 0.25rem; - font-size: 0.8125rem; - color: var(--bbk-color-muted); -} - -/* A line's custom-field answers (checkout::partials.line-custom-fields). */ -.bbk-line-fields { - display: grid; - gap: 0.25rem; - margin: 0 0 0.5rem; - font-size: 0.8125rem; -} - -.bbk-line-field dt { - color: var(--bbk-color-muted); -} - -.bbk-line-field dd { - margin: 0; - white-space: pre-line; - overflow-wrap: anywhere; -} - -.bbk-line-field-file { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: inherit; -} - -.bbk-line-field-file img { - width: 40px; - height: 40px; - object-fit: cover; - border-radius: 0; -} - -.bbk-cart-item-unit { - margin: 0 0 0.625rem; - color: var(--bbk-color-muted); -} - -.bbk-cart-item-aside { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 0.5rem; -} - -.bbk-cart-item-total { margin: 0; font-weight: 600; } - -.bbk-cart-item-remove { - font-size: 1.125rem; - width: 1.5rem; - height: 1.5rem; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.bbk-cart-qty { - display: inline-flex; - align-items: center; - gap: 0; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - overflow: hidden; -} - -.bbk-cart-qty-btn { - width: 1.75rem; - height: 1.75rem; - background: var(--bbk-color-bg-muted); -} - -.bbk-cart-qty-btn:hover { background: var(--bbk-color-border); color: var(--bbk-color-text); } - -.bbk-cart-qty-input { - width: 2.25rem; - height: 1.75rem; - border: 0; - border-left: 1px solid var(--bbk-color-border); - border-right: 1px solid var(--bbk-color-border); - text-align: center; - font: inherit; - color: inherit; - background: var(--bbk-color-bg); - appearance: textfield; - -moz-appearance: textfield; -} - -.bbk-cart-qty-input::-webkit-outer-spin-button, -.bbk-cart-qty-input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.bbk-cart-summary { - padding-top: 1.25rem; - border-top: 1px solid var(--bbk-color-border); - display: flex; - flex-direction: column; - gap: 0.625rem; -} - -.bbk-cart-summary-row { - display: flex; - justify-content: space-between; - gap: 1rem; -} - -.bbk-cart-summary-row--discount { color: var(--bbk-color-danger); } - -.bbk-cart-summary-pending { - color: var(--bbk-color-muted); - font-size: 0.8125rem; -} - -.bbk-cart-summary-row--total { - margin-top: 0.375rem; - padding-top: 0.875rem; - border-top: 1px solid var(--bbk-color-border); - font-size: 1.0625rem; - font-weight: 700; -} - -.bbk-cart-coupon-form { - display: flex; - gap: 0.5rem; -} - -.bbk-cart-coupon-input { - flex: 1 1 auto; - min-width: 0; - padding: 0.5rem 0.75rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - font: inherit; - color: inherit; - background: var(--bbk-color-bg); -} - -.bbk-cart-coupon-submit { - flex: 0 0 auto; - padding: 0.5rem 0.875rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - background: var(--bbk-color-bg-muted); - font: inherit; - font-weight: 600; - cursor: pointer; - transition: background-color 0.15s ease, border-color 0.15s ease; -} - -.bbk-cart-coupon-submit:hover { background: var(--bbk-color-border); } - -.bbk-cart-coupon-applied { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; - padding: 0.625rem 0.875rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - background: var(--bbk-color-bg-muted); -} - -.bbk-cart-coupon-code { - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.02em; -} - -.bbk-cart-coupon-remove { - flex: 0 0 auto; - background: none; - border: 0; - padding: 0; - font: inherit; - font-size: 0.8125rem; - color: var(--bbk-color-muted); - text-decoration: underline; - cursor: pointer; - transition: color 0.15s ease; -} - -.bbk-cart-coupon-remove:hover { color: var(--bbk-color-danger); } - -.bbk-cart-coupon-error { - margin: 0.5rem 0 0; - font-size: 0.8125rem; - color: var(--bbk-color-danger); -} - -.bbk-cart-error { - margin: 0; - padding: 0.75rem 1.5rem 0; - font-size: 0.8125rem; - color: var(--bbk-color-danger); -} - -.bbk-add-to-cart-error { - margin: 0.375rem 0 0; - font-size: 0.8125rem; - color: var(--bbk-color-danger); -} - -.bbk-cart-checkout { - display: block; - width: 100%; - padding: 0.875rem 1.25rem; - border: 1px solid var(--bbk-color-accent); - border-radius: var(--bbk-radius); - background: var(--bbk-color-accent); - color: var(--bbk-color-accent-text); - font: inherit; - font-weight: 600; - text-align: center; - text-decoration: none; - cursor: pointer; - transition: opacity 0.15s ease; -} - -.bbk-cart-checkout:hover { opacity: 0.85; } - -.bbk-cart-checkout:disabled { - cursor: not-allowed; - opacity: 0.4; -} - -.bbk-cart-empty { - text-align: center; - color: var(--bbk-color-muted); - padding: 2.5rem 0; -} - -/* ─────────────────────────────────────────────────────────────────── - Checkout page — two columns: fields on the left, order summary (the - same cart-body partial the drawer uses) on the right. - ─────────────────────────────────────────────────────────────────── */ - -.bbk-checkout-page { - max-width: 1100px; - margin: 0 auto; - padding: 2.5rem 1.5rem 5rem; - font-family: var(--bbk-font); - font-size: 0.9375rem; - line-height: 1.4; - color: var(--bbk-color-text); -} - -.bbk-checkout-heading { - margin: 0 0 2rem; - font-size: 1.75rem; - font-weight: 700; -} - -.bbk-checkout { - display: grid; - grid-template-columns: 1fr 380px; - gap: 3rem; - align-items: start; -} - -@media (max-width: 860px) { - .bbk-checkout { grid-template-columns: 1fr; } -} - -.bbk-checkout-main { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.bbk-checkout-section { - padding-bottom: 2rem; - border-bottom: 1px solid var(--bbk-color-border); - display: flex; - flex-direction: column; - gap: 1rem; -} - -.bbk-checkout-section-heading { - margin: 0; - font-size: 1.125rem; - font-weight: 700; -} - -.bbk-checkout-note { - margin: 0; - color: var(--bbk-color-muted); - font-size: 0.875rem; -} - -/* Contact: "logged in as" line, or the guest login prompt */ - -.bbk-checkout-logged-in, -.bbk-checkout-login-prompt { margin: 0; } - -.bbk-checkout-login-prompt a { color: inherit; font-weight: 600; } - -/* Fields */ - -.bbk-field { display: flex; flex-direction: column; gap: 0.375rem; } - -.bbk-field-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; -} - -@media (max-width: 480px) { - .bbk-field-row { grid-template-columns: 1fr; } -} - -.bbk-field-label { - font-size: 0.8125rem; - font-weight: 600; - color: var(--bbk-color-muted); -} - -.bbk-field-input { - padding: 0.625rem 0.75rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - font: inherit; - color: inherit; - background: var(--bbk-color-bg); -} - -.bbk-field-input:focus-visible { - outline: 2px solid var(--bbk-color-accent); - outline-offset: 2px; -} - -.bbk-field-input:disabled { - background: var(--bbk-color-bg-muted); - color: var(--bbk-color-muted); -} - -.bbk-field-input--error { border-color: var(--bbk-color-danger); } - -.bbk-field-error { - margin: 0; - font-size: 0.8125rem; - color: var(--bbk-color-danger); -} - -/* A fixed, non-editable field value (e.g. the store's single country). */ -.bbk-field-static { - margin: 0; - padding: 0.625rem 0.75rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - background: var(--bbk-color-bg-muted); - color: var(--bbk-color-muted); -} - -textarea.bbk-field-input { resize: vertical; } - -.bbk-checkbox { - display: inline-flex; - align-items: center; - gap: 0.5rem; - font-size: 0.875rem; - cursor: pointer; -} - -/* For a full-sentence label that can wrap — align the box to the first line. */ -/* "I want an invoice": company/ΑΦΜ only while ticked */ - -.bbk-invoice { display: flex; flex-direction: column; gap: 1rem; } - -.bbk-invoice:not(:has(input[name="wants_invoice"]:checked)) .bbk-invoice-fields { display: none; } - -.bbk-checkbox--stacked { - display: flex; - align-items: flex-start; - margin-top: 0.75rem; - color: var(--bbk-color-muted); -} - -.bbk-checkbox--stacked input { margin-top: 0.15rem; flex-shrink: 0; } - -.bbk-checkout-shipping-fields { - display: flex; - flex-direction: column; - gap: 1rem; -} - -/* Shipping method */ - -.bbk-checkout-shipping-options { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.bbk-checkout-shipping-option { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.875rem 1rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - cursor: pointer; - transition: border-color 0.15s ease; -} - -.bbk-checkout-shipping-option:has(input:checked) { border-color: var(--bbk-color-accent); } - -.bbk-checkout-shipping-option-detail { - flex: 1 1 auto; - display: flex; - flex-direction: column; - gap: 0.125rem; -} - -.bbk-checkout-shipping-option-name { font-weight: 600; } - -.bbk-checkout-shipping-option-description { - font-size: 0.8125rem; - color: var(--bbk-color-muted); -} - -.bbk-checkout-shipping-option-price { font-weight: 600; } - -/* The single auto-selected option — a fixed line, not a choosable radio. */ -.bbk-checkout-shipping-confirmed { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.875rem 1rem; - border: 1px solid var(--bbk-color-accent); - border-radius: var(--bbk-radius-sm); -} - -/* Autosave status line under the address form. */ -.bbk-checkout-status { - margin: 0; - font-size: 0.8125rem; - color: var(--bbk-color-muted); -} - -.bbk-checkout-status[data-state="error"] { color: var(--bbk-color-danger); } - -/* Dummy Box Now locker picker — see shipping-options.blade.php. */ -.bbk-checkout-box-now-locker { - display: flex; - flex-direction: column; - gap: 0.5rem; - margin-top: 0.75rem; - padding: 0.875rem 1rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); -} - -.bbk-checkout-box-now-locker-label { - font-weight: 600; - font-size: 0.8125rem; -} - -.bbk-checkout-box-now-locker-map { - width: 100%; - height: 480px; - border-radius: var(--bbk-radius-sm); - z-index: 0; -} - -.bbk-checkout-box-now-locker-search { - width: 100%; - padding: 0.625rem 0.875rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - font-size: 0.875rem; -} - -.bbk-checkout-box-now-locker-search:focus { - outline: none; - border-color: var(--bbk-color-accent); -} - -.bbk-checkout-box-now-locker-chosen { - margin: 0; - font-size: 0.8125rem; - font-weight: 600; - color: var(--bbk-color-accent); -} - -/* Custom SVG pin (see bbk-box-now-locker-controller.js pinIcon()) — no - default Leaflet drop-shadow image, so a CSS shadow stands in for it. */ -.bbk-box-now-pin svg { - filter: drop-shadow(0 2px 3px rgb(0 0 0 / 0.35)); -} - -/* Leaflet's own popup chrome, restyled to match the checkout's cards - instead of the library's square-cornered default. */ -.leaflet-popup-content-wrapper { - border-radius: 0.75rem; - box-shadow: 0 8px 24px rgb(0 0 0 / 0.18); -} - -.leaflet-popup-content { - margin: 0.875rem; -} - -.bbk-box-now-popup { - display: flex; - flex-direction: column; - gap: 0.5rem; - min-width: 220px; -} - -.bbk-box-now-popup-image { - width: calc(100% + 1.75rem); - margin: -0.875rem -0.875rem 0.125rem; - height: 110px; - object-fit: cover; - border-radius: 0.75rem 0.75rem 0 0; -} - -.bbk-box-now-popup-name { - display: flex; - align-items: center; - gap: 0.375rem; - margin: 0; - font-weight: 700; - font-size: 0.9375rem; -} - -.bbk-box-now-popup-name::before { - content: ''; - flex: 0 0 auto; - width: 0.5rem; - height: 0.5rem; - border-radius: 999px; - background: #00c389; -} - -.bbk-box-now-popup-address { - margin: 0; - padding-left: 0.875rem; - font-size: 0.8125rem; - line-height: 1.4; - color: var(--bbk-color-muted); -} - -.bbk-box-now-popup-note { - margin: 0 0 0 0.875rem; - padding: 0.5rem 0.625rem; - background: color-mix(in srgb, #00c389 8%, transparent); - border-radius: var(--bbk-radius-sm); - font-size: 0.75rem; - font-style: italic; - color: var(--bbk-color-muted); -} - -.bbk-box-now-popup-select { - margin-top: 0.25rem; - padding: 0.625rem 0.875rem; - width: 100%; - border: none; - border-radius: var(--bbk-radius-sm); - background: #00c389; - color: #ffffff; - font-weight: 700; - font-size: 0.8125rem; - letter-spacing: 0.01em; - cursor: pointer; - transition: background-color 0.15s ease, transform 0.1s ease; -} - -.bbk-box-now-popup-select:hover { background: #00a876; transform: translateY(-1px); } -.bbk-box-now-popup-select:active { transform: translateY(0); } - -.bbk-box-now-popup-select--selected, -.bbk-box-now-popup-select--selected:hover { - background: var(--bbk-color-muted); - cursor: default; -} - -/* Continue / submit buttons — same look as the drawer's checkout CTA */ - -.bbk-checkout-continue { - display: block; - width: 100%; - padding: 0.875rem 1.25rem; - border: 1px solid var(--bbk-color-accent); - border-radius: var(--bbk-radius); - background: var(--bbk-color-accent); - color: var(--bbk-color-accent-text); - font: inherit; - font-weight: 600; - text-align: center; - text-decoration: none; - box-sizing: border-box; - cursor: pointer; - transition: opacity 0.15s ease; -} - -.bbk-checkout-continue:hover { opacity: 0.85; } - -.bbk-checkout-continue:disabled { - cursor: not-allowed; - opacity: 0.4; -} - -/* Order summary column */ - -.bbk-checkout-aside { position: sticky; top: 1.5rem; } - -.bbk-checkout-summary { - padding: 1.5rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius); - background: var(--bbk-color-bg); -} - -.bbk-checkout-summary-heading { - margin: 0 0 1.25rem; - font-size: 1.125rem; - font-weight: 700; -} - -/* Already on the checkout page — the drawer's own "go to checkout" CTA has - nowhere further to send you from here. */ -.bbk-checkout-summary .bbk-cart-checkout { display: none; } - -/* ── Payment ───────────────────────────────────────────────────────── */ - -.bbk-checkout-payment-options { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.bbk-checkout-payment-option { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.875rem 1rem; - border: 1px solid var(--bbk-color-border); - border-radius: var(--bbk-radius-sm); - cursor: pointer; - transition: border-color 0.15s ease; -} - -.bbk-checkout-payment-option:has(input:checked) { border-color: var(--bbk-color-accent); } - -.bbk-checkout-payment-option-name { font-weight: 600; } - -.bbk-payment-element { margin: 0.25rem 0; } - -.bbk-checkout-withdrawal { - margin: 0; - font-size: 0.8125rem; - color: var(--bbk-color-muted); -} - -.bbk-checkout-withdrawal a { color: inherit; } - -.bbk-checkout-error { - margin: 0; - font-size: 0.875rem; - color: var(--bbk-color-danger); -} - -/* Processing overlay — fixed, covers the page while a payment confirms. */ -.bbk-checkout-processing { - position: fixed; - inset: 0; - z-index: 1100; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - background: color-mix(in srgb, var(--bbk-color-bg) 92%, transparent); - text-align: center; - padding: 1.5rem; -} - -.bbk-spinner { - width: 2rem; - height: 2rem; - border: 3px solid var(--bbk-color-border); - border-top-color: var(--bbk-color-accent); - border-radius: 50%; - animation: bbk-spin 0.8s linear infinite; -} - -@keyframes bbk-spin { - to { transform: rotate(360deg); } -} - -/* ── Confirmation page ─────────────────────────────────────────────── */ - -.bbk-confirmation { - max-width: 720px; - margin: 0 auto; - padding: 3rem 1.5rem 5rem; - font-family: var(--bbk-font); - color: var(--bbk-color-text); -} - -.bbk-confirmation-heading { - margin: 0 0 1rem; - font-size: 1.75rem; - font-weight: 700; -} - -.bbk-confirmation-ref { margin: 0 0 0.25rem; } - -.bbk-confirmation-meta { - margin: 0 0 1rem; - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.bbk-confirmation-meta-row { - display: flex; - justify-content: space-between; - gap: 1rem; - font-size: 0.9375rem; -} - -.bbk-confirmation-meta-row dt { color: var(--bbk-color-muted); } -.bbk-confirmation-meta-row dd { margin: 0; font-weight: 600; } - -.bbk-confirmation-body { - margin: 2rem 0; - display: grid; - gap: 2.5rem; -} - -@media (min-width: 640px) { - .bbk-confirmation-body { grid-template-columns: 1fr 1fr; } -} - -.bbk-confirmation-lines { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.bbk-confirmation-line { - display: grid; - grid-template-columns: 72px 1fr auto; - align-items: start; - gap: 0.875rem; -} - -.bbk-confirmation-line-detail { min-width: 0; } - -.bbk-confirmation-line-qty { color: var(--bbk-color-muted); } - -.bbk-confirmation-lines .bbk-cart-summary { margin-top: 0.75rem; } - -.bbk-confirmation-addresses { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.bbk-confirmation-address-heading { - margin: 0 0 0.5rem; - font-size: 0.9375rem; - font-weight: 700; -} - -.bbk-address-lines { - font-style: normal; - display: flex; - flex-direction: column; - gap: 0.125rem; - font-size: 0.875rem; - color: var(--bbk-color-muted); -} diff --git a/resources/js/checkout/bbk-add-to-cart-controller.js b/resources/js/checkout/bbk-add-to-cart-controller.js deleted file mode 100644 index 1f1cdf2..0000000 --- a/resources/js/checkout/bbk-add-to-cart-controller.js +++ /dev/null @@ -1,57 +0,0 @@ -import { Controller } from '@hotwired/stimulus' -import { csrfToken } from './csrf' - -// Sits on an
. Submits the line to the cart -// via fetch and hands the server-rendered cart body to the drawer through the -// `bbk-cart:changed` window event. No DOM building here — the drawer -// (bbk-cart-controller) owns rendering. -export default class extends Controller { - static targets = ['error'] - - async add(event) { - event.preventDefault() - - const form = this.element - const submit = form.querySelector('[type="submit"]') - - this.clearError() - form.setAttribute('data-bbk-add-to-cart-state', 'loading') - if (submit) submit.disabled = true - - try { - const response = await fetch(form.action, { - method: 'POST', - headers: { - 'X-CSRF-TOKEN': csrfToken(), - 'X-Requested-With': 'XMLHttpRequest', - Accept: 'application/json', - }, - body: new FormData(form), - }) - - if (!response.ok) { - const data = await response.json().catch(() => null) - this.showError(data?.error) - return - } - - window.dispatchEvent(new CustomEvent('bbk-cart:changed', { - detail: { html: await response.text() }, - })) - } finally { - form.removeAttribute('data-bbk-add-to-cart-state') - if (submit) submit.disabled = false - } - } - - showError(message) { - if (!this.hasErrorTarget || !message) return - this.errorTarget.textContent = message - this.errorTarget.hidden = false - } - - clearError() { - if (!this.hasErrorTarget) return - this.errorTarget.hidden = true - } -} diff --git a/resources/js/checkout/bbk-box-now-locker-controller.js b/resources/js/checkout/bbk-box-now-locker-controller.js deleted file mode 100644 index 37b649e..0000000 --- a/resources/js/checkout/bbk-box-now-locker-controller.js +++ /dev/null @@ -1,220 +0,0 @@ -import { Controller } from '@hotwired/stimulus' -import L from 'leaflet' -import 'leaflet/dist/leaflet.css' -import { csrfToken } from './csrf' - -// Box Now's own brand green, used for the pin instead of Leaflet's default -// blue teardrop — a small SVG data URI rather than another bundled asset. -const PIN_COLOR = '#00c389' -const PIN_COLOR_SELECTED = '#0a7a52' - -function pinIcon(color) { - const svg = ` - - - - - ` - - return L.divIcon({ - className: 'bbk-box-now-pin', - html: svg, - iconSize: [34, 46], - iconAnchor: [17, 46], - popupAnchor: [0, -40], - }) -} - -const ICON = pinIcon(PIN_COLOR) -const ICON_SELECTED = pinIcon(PIN_COLOR_SELECTED) - -// A self-hosted Leaflet map standing in for Box Now's own Destination Map -// JS widget — that widget only talks to Box Now's Production API (see -// their Partner API manual §4.1), so it can't be used while developing -// against Stage credentials. Same underlying /destinations data, rendered -// with OpenStreetMap tiles instead of Box Now's map. -// -// Visibility is toggled by bbk-checkout-form (see its own -// toggleBoxNowLocker()) whenever the "box-now" shipping option becomes -// selected/deselected — this controller only owns loading the locker list -// once visible, rendering pins, and autosaving the chosen one. -export default class extends Controller { - static targets = ['map', 'search', 'status', 'chosen'] - - static values = { - lockersUrl: String, - selectUrl: String, - loading: String, - selectLabel: String, - selectedLabel: String, - noResults: String, - } - - // Athens — a reasonable default center before any locker is loaded. - static DEFAULT_CENTER = [37.9838, 23.7275] - - connect() { - this.map = null - this.markers = new Map() - this.selectedId = null - this.loaded = false - - if (!this.element.hidden) this.show() - } - - disconnect() { - this.map?.remove() - this.map = null - } - - // Called by bbk-checkout-form right after it un-hides this element. - show() { - this.element.hidden = false - - // Leaflet measures its container's size on init — doing that while - // the element (or an ancestor) is still `hidden` produces a - // collapsed/blank map, so this is deferred to the same tick `hidden` - // is cleared, then Leaflet is nudged once more via invalidateSize(). - requestAnimationFrame(() => { - if (!this.map) this.initMap() - this.map.invalidateSize() - - if (!this.loaded) this.loadLockers() - }) - } - - hide() { - this.element.hidden = true - } - - initMap() { - this.map = L.map(this.mapTarget).setView(this.constructor.DEFAULT_CENTER, 10) - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', - maxZoom: 19, - }).addTo(this.map) - - // Delegated: popup content is re-inserted by Leaflet on every open, - // so a listener bound once on the map's container beats binding (and - // losing) one on the button each time a popup renders. - this.map.getContainer().addEventListener('click', (event) => { - const button = event.target.closest('[data-locker-id]') - if (button) this.select(button.dataset.lockerId) - }) - } - - async loadLockers() { - this.setStatus(this.loadingValue) - - try { - const response = await fetch(this.lockersUrlValue, { - headers: { Accept: 'application/json' }, - }) - - if (!response.ok) return - - const { lockers } = await response.json() - this.loaded = true - this.lockers = new Map(lockers.map((locker) => [String(locker.id), locker])) - this.renderMarkers(lockers) - this.setStatus('') - } catch { - this.setStatus('') - } - } - - renderMarkers(lockers) { - this.markers.forEach((marker) => marker.remove()) - this.markers = new Map(lockers.map((locker) => { - const marker = L.marker([locker.lat, locker.lng], { icon: ICON }) - .addTo(this.map) - .bindPopup(this.popupHtml(locker), { maxWidth: 260 }) - - return [String(locker.id), marker] - })) - - if (this.markers.size) { - this.map.fitBounds(L.featureGroup([...this.markers.values()]).getBounds().pad(0.2)) - } - } - - popupHtml(locker) { - const isSelected = String(locker.id) === this.selectedId - - return ` -
- ${locker.image ? `` : ''} -

${locker.name}

-

- ${[locker.addressLine1, locker.addressLine2].filter(Boolean).join(', ')} - ${locker.postalCode ? ` ${locker.postalCode}` : ''} -

- ${locker.note ? `

${locker.note}

` : ''} - -
- ` - } - - async select(lockerId) { - const locker = this.lockers?.get(String(lockerId)) - if (!locker) return - - const previousId = this.selectedId - this.selectedId = String(lockerId) - - this.restyleMarker(previousId, ICON) - this.restyleMarker(this.selectedId, ICON_SELECTED) - this.markers.get(this.selectedId)?.setPopupContent(this.popupHtml(locker)) - - this.chosenTarget.hidden = false - this.chosenTarget.textContent = locker.addressLine1 - ? `${locker.name} — ${locker.addressLine1}` - : locker.name - - const body = new FormData() - body.append('locker_id', locker.id) - body.append('locker_name', locker.name ?? '') - body.append('locker_address', locker.addressLine1 ?? '') - - try { - await fetch(this.selectUrlValue, { - method: 'POST', - headers: { - 'X-CSRF-TOKEN': csrfToken(), - 'X-Requested-With': 'XMLHttpRequest', - Accept: 'application/json', - }, - body, - }) - } catch { - // Best-effort autosave, same as the rest of checkout — a failed - // save here surfaces later at place-order time via the normal - // shipment-creation error path, not as an inline field error. - } - } - - restyleMarker(lockerId, icon) { - if (!lockerId) return - this.markers.get(lockerId)?.setIcon(icon) - } - - setStatus(text) { - if (!this.hasStatusTarget) return - - this.statusTarget.textContent = text - this.statusTarget.hidden = !text - } -} diff --git a/resources/js/checkout/bbk-cart-controller.js b/resources/js/checkout/bbk-cart-controller.js deleted file mode 100644 index dfe5901..0000000 --- a/resources/js/checkout/bbk-cart-controller.js +++ /dev/null @@ -1,161 +0,0 @@ -import { Controller } from '@hotwired/stimulus' -import { csrfToken } from './csrf' - -// Drives the slide-in cart drawer. One instance, on the drawer root in -// checkout/drawer.blade.php. -// -// - listens on window for `bbk-cart:changed` (from bbk-add-to-cart and from -// this drawer's own line forms) and swaps in the server-rendered cart body -// - handles the in-drawer quantity / remove forms (fetch + method spoofing) -// - re-emits `bbk-cart:updated` {count, total} after every render so the host -// (e.g. the header bag icon) can react -// -// Appearance is entirely CSS-driven: open state is the data-bbk-cart-state -// attribute on the root, nothing here touches styles or class lists. -export default class extends Controller { - static targets = ['panel', 'body', 'error'] - - connect() { - this.onChanged = this.onChanged.bind(this) - this.onKeydown = this.onKeydown.bind(this) - this.updateTimers = new Map() // line id -> pending debounce timer - - window.addEventListener('bbk-cart:changed', this.onChanged) - window.addEventListener('bbk-cart:open', this.open.bind(this)) - document.addEventListener('keydown', this.onKeydown) - - // Prime the host with the count rendered server-side on page load. - this.emitUpdated(this.element.querySelector('[data-bbk-cart-count]')) - } - - disconnect() { - window.removeEventListener('bbk-cart:changed', this.onChanged) - document.removeEventListener('keydown', this.onKeydown) - this.updateTimers.forEach((timer) => clearTimeout(timer)) - } - - onChanged(event) { - if (event.detail?.html) this.replaceBody(event.detail.html) - this.open() - } - - onKeydown(event) { - if (event.key === 'Escape' && !this.element.hidden) this.close() - } - - open() { - if (!this.element.hidden) return - this.element.hidden = false - // Next frame, so the panel transitions from its off-canvas start. - requestAnimationFrame(() => this.element.setAttribute('data-bbk-cart-state', 'open')) - } - - close() { - this.element.removeAttribute('data-bbk-cart-state') - - const panel = this.panelTarget - const done = () => { - this.element.hidden = true - panel.removeEventListener('transitionend', done) - } - panel.addEventListener('transitionend', done) - } - - // change on a line quantity input, or submit of a line's remove form - submit(event) { - event.preventDefault() - const form = event.target.closest('form') - if (!form) return - - // A remove is a deliberate, one-shot action — only the quantity form - // (typing, or the +/- stepper below) benefits from debouncing. - form.classList.contains('bbk-cart-qty') ? this.scheduleSend(form) : this.send(form) - } - - // +/- stepper buttons inside a line - step(event) { - event.preventDefault() - const form = event.target.closest('form') - const input = form.querySelector('input[type="number"]') - const next = Math.max(0, parseInt(input.value || '0', 10) + Number(event.params.dir)) - input.value = String(next) - this.scheduleSend(form) - } - - // Repeated clicks (or spinner nudges) update the input instantly but only - // send once they settle for 300ms — sending on every single click was - // firing overlapping requests that raced each other and made the drawer - // visibly flicker/lag under quick clicking. - scheduleSend(form) { - const lineId = form.closest('[data-bbk-line-id]')?.dataset.bbkLineId - if (!lineId) return this.send(form) - - clearTimeout(this.updateTimers.get(lineId)) - this.updateTimers.set(lineId, setTimeout(() => { - this.updateTimers.delete(lineId) - this.send(form) - }, 300)) - } - - async send(form) { - this.bodyTarget.setAttribute('aria-busy', 'true') - this.clearError() - - try { - const response = await fetch(form.action, { - method: 'POST', - headers: { - 'X-CSRF-TOKEN': csrfToken(), - 'X-Requested-With': 'XMLHttpRequest', - Accept: 'application/json', - }, - body: new FormData(form), - }) - - if (response.ok) { - this.replaceBody(await response.text()) - return - } - - const data = await response.json().catch(() => null) - this.showError(data?.error) - - // The rejected quantity (typed, or from a +/- click) is left - // sitting in the input with nothing to correct it — the update - // never reached the cart, so the input must be put back to what - // the cart actually still holds, not just left showing whatever - // was rejected. - const input = form.querySelector('[data-bbk-cart-confirmed-quantity]') - if (input) input.value = input.dataset.bbkCartConfirmedQuantity - } finally { - this.bodyTarget.removeAttribute('aria-busy') - } - } - - showError(message) { - if (!this.hasErrorTarget || !message) return - this.errorTarget.textContent = message - this.errorTarget.hidden = false - } - - clearError() { - if (!this.hasErrorTarget) return - this.errorTarget.hidden = true - } - - replaceBody(html) { - this.bodyTarget.innerHTML = html - this.emitUpdated(this.bodyTarget.querySelector('[data-bbk-cart-count]')) - } - - emitUpdated(node) { - if (!node) return - - window.dispatchEvent(new CustomEvent('bbk-cart:updated', { - detail: { - count: parseInt(node.dataset.bbkCartCount || '0', 10), - total: parseInt(node.dataset.bbkCartTotal || '0', 10), - }, - })) - } -} diff --git a/resources/js/checkout/bbk-checkout-form-controller.js b/resources/js/checkout/bbk-checkout-form-controller.js deleted file mode 100644 index aa310ec..0000000 --- a/resources/js/checkout/bbk-checkout-form-controller.js +++ /dev/null @@ -1,226 +0,0 @@ -import { Controller } from '@hotwired/stimulus' -import { csrfToken } from './csrf' - -// Drives the checkout page's left column: contact tabs, the same-as-billing -// toggle, and — the bulk of it — autosaving the address form and the shipping -// method with no submit buttons. -// -// Flow: any `change` in the address form is debounced ~400ms, then the whole -// form is POSTed to saveUrl. The server persists leniently and returns -// { errors, shippingOptionsHtml, summaryHtml }. We swap the shipping-options -// block in place and hand the summary fragment to the drawer's bbk-cart -// controller via the `bbk-cart:changed` window event (same mechanism the drawer -// already uses). Shipping-method radios post to selectShippingUrl the same way. -export default class extends Controller { - static targets = [ - 'sameAsBilling', 'shippingFields', - 'form', 'shippingOptions', 'status', - ] - - static values = { - saveUrl: String, - selectShippingUrl: String, - statusSaving: String, - statusSaved: String, - statusError: String, - } - - connect() { - this.saveTimer = null - this.saveController = null - this.statusTimer = null - this.shippingPromise = null - - if (this.hasSameAsBillingTarget) this.applySameAsBilling() - } - - disconnect() { - clearTimeout(this.saveTimer) - clearTimeout(this.statusTimer) - this.saveController?.abort() - } - - // ── Same as billing ──────────────────────────────────────────────── - - toggleSameAsBilling() { - this.applySameAsBilling() - } - - applySameAsBilling() { - const on = this.sameAsBillingTarget.checked - - // Checked: shipping *is* billing — copy every value across, then hide + - // disable so the browser doesn't submit them; the server reuses billing. - // Unchecked: reveal them pre-filled from billing wherever still empty. - this.element.querySelectorAll('[name^="billing_"]').forEach((billingField) => { - const shippingField = this.element.querySelector( - `[name="${billingField.name.replace(/^billing_/, 'shipping_')}"]`, - ) - if (shippingField && (on || !shippingField.value)) { - shippingField.value = billingField.value - } - }) - - this.shippingFieldsTarget.hidden = on - this.shippingFieldsTarget.querySelectorAll('input, select, textarea').forEach((field) => { - field.disabled = on - }) - } - - // ── Autosave ─────────────────────────────────────────────────────── - - scheduleSave(event) { - // The shipping-method and payment radios live inside this controller's - // element too, and this action is bound on .bbk-checkout-main to also - // catch the contact email/consent that sit outside the . Only - // react to fields that actually belong to the address form. - const el = event.target - const belongsToForm = el.form?.id === 'bbk-address-form' - if (!belongsToForm) return - - // No status during the wait — it only shows once the request is in flight, - // so the indicator isn't flickering "saving" on every keystroke. - clearTimeout(this.saveTimer) - this.saveTimer = setTimeout(() => this.save(), 700) - } - - // Called by bbk-payment right before place-order — a debounced save (and - // the shipping-option auto-select that happens as part of it) might still - // be pending when the shopper clicks "place order"; this guarantees the - // server has processed the current form state first. - async flush() { - clearTimeout(this.saveTimer) - await this.save() - // A shipping-method radio click fires its own (undebounced) request — - // still async, still racy against an immediate "place order" click. - if (this.shippingPromise) await this.shippingPromise - } - - async save() { - this.saveController?.abort() - this.saveController = new AbortController() - this.setStatus('saving') - - try { - const response = await fetch(this.saveUrlValue, { - method: 'POST', - headers: { - 'X-CSRF-TOKEN': csrfToken(), - 'X-Requested-With': 'XMLHttpRequest', - Accept: 'application/json', - }, - body: new FormData(this.formTarget), - signal: this.saveController.signal, - }) - - if (!response.ok) return this.setStatus('error') - - this.applyResult(await response.json()) - this.setStatus('saved') - } catch (error) { - if (error.name !== 'AbortError') this.setStatus('error') - } - } - - async selectShipping(event) { - this.toggleBoxNowLocker(event.target.value) - - // Tracked so flush() can await it — nothing else stops "place order" - // (a separate, unrelated click) from racing ahead of this request. - this.shippingPromise = this.doSelectShipping(event.target.value) - await this.shippingPromise - } - - // The dummy Box Now locker