diff --git a/app/Http/Controllers/Checkout/CheckoutController.php b/app/Http/Controllers/Checkout/CheckoutController.php new file mode 100644 index 0000000..11ce674 --- /dev/null +++ b/app/Http/Controllers/Checkout/CheckoutController.php @@ -0,0 +1,157 @@ +cart->current(); + $lines = $cart ? $this->cart->activeLines($cart) : collect(); + + $shippingAddress = $cart?->shippingAddress; + $billingAddress = $cart?->billingAddress; + + // Rate quoting needs a shipping address to resolve against (postcode/ + // country for ACS/BoxNow live rates, table-rate-shipping's zones) — + // nothing to offer yet on a cart with no address saved. + $shippingOptions = $shippingAddress + ? $this->checkout->getShippingOptions() + : collect(); + + return view('checkout::page', [ + 'cart' => $cart, + 'lines' => $lines, + 'billingAddress' => $billingAddress, + 'shippingAddress' => $shippingAddress, + 'shippingOptions' => $shippingOptions, + 'countries' => Country::orderBy('name')->get(['id', 'name']), + ]); + } + + /** + * One form, two CheckoutService calls — billing and shipping are always + * both set together here. When "same as billing" is checked the shipping + * fields are disabled client-side (so the browser never submits them) and + * the billing values are reused for shipping too, plus delivery + * instructions. + */ + public function saveAddress(string $locale, Request $request): RedirectResponse + { + $sameAsBilling = $request->boolean('same_as_billing'); + + $rules = [ + 'contact_email' => ['required', 'email'], + + 'billing_first_name' => ['required', 'string', 'max:255'], + 'billing_last_name' => ['required', 'string', 'max:255'], + 'billing_company_name' => ['nullable', 'string', 'max:255'], + 'billing_tax_identifier' => ['nullable', 'string', 'max:255'], + 'billing_line_one' => ['required', 'string', 'max:255'], + 'billing_line_two' => ['nullable', 'string', 'max:255'], + 'billing_city' => ['required', 'string', 'max:255'], + 'billing_state' => ['nullable', 'string', 'max:255'], + 'billing_postcode' => ['required', 'string', 'max:20'], + 'billing_country_id' => ['required', 'integer', 'exists:'.(new Country)->getTable().',id'], + 'billing_contact_phone' => ['nullable', 'string', 'max:50'], + + 'shipping_delivery_instructions' => ['nullable', 'string', 'max:1000'], + ]; + + if (! $sameAsBilling) { + $rules += [ + 'shipping_first_name' => ['required', 'string', 'max:255'], + 'shipping_last_name' => ['required', 'string', 'max:255'], + 'shipping_company_name' => ['nullable', 'string', 'max:255'], + 'shipping_line_one' => ['required', 'string', 'max:255'], + 'shipping_line_two' => ['nullable', 'string', 'max:255'], + 'shipping_city' => ['required', 'string', 'max:255'], + 'shipping_state' => ['nullable', 'string', 'max:255'], + 'shipping_postcode' => ['required', 'string', 'max:20'], + 'shipping_country_id' => ['required', 'integer', 'exists:'.(new Country)->getTable().',id'], + 'shipping_contact_phone' => ['nullable', 'string', 'max:50'], + ]; + } + + $data = $request->validate($rules); + + $billing = [ + 'first_name' => $data['billing_first_name'], + 'last_name' => $data['billing_last_name'], + 'company_name' => $data['billing_company_name'] ?? null, + 'tax_identifier' => $data['billing_tax_identifier'] ?? null, + 'line_one' => $data['billing_line_one'], + 'line_two' => $data['billing_line_two'] ?? null, + 'city' => $data['billing_city'], + 'state' => $data['billing_state'] ?? null, + 'postcode' => $data['billing_postcode'], + 'country_id' => $data['billing_country_id'], + 'contact_email' => $data['contact_email'], + 'contact_phone' => $data['billing_contact_phone'] ?? null, + ]; + + $shipping = $sameAsBilling + ? [...$billing, 'delivery_instructions' => $data['shipping_delivery_instructions'] ?? null] + : [ + 'first_name' => $data['shipping_first_name'], + 'last_name' => $data['shipping_last_name'], + 'company_name' => $data['shipping_company_name'] ?? null, + 'line_one' => $data['shipping_line_one'], + 'line_two' => $data['shipping_line_two'] ?? null, + 'city' => $data['shipping_city'], + 'state' => $data['shipping_state'] ?? null, + 'postcode' => $data['shipping_postcode'], + 'country_id' => $data['shipping_country_id'], + 'contact_email' => $data['contact_email'], + 'contact_phone' => $data['shipping_contact_phone'] ?? null, + 'delivery_instructions' => $data['shipping_delivery_instructions'] ?? null, + ]; + + $this->checkout->setBillingAddress($billing); + $this->checkout->setShippingAddress($shipping); + + return redirect()->route('checkout.show', $locale); + } + + public function selectShippingOption(string $locale, Request $request): RedirectResponse + { + $data = $request->validate([ + 'shipping_option' => ['required', 'string'], + ]); + + try { + $this->checkout->selectShippingOption($data['shipping_option']); + } catch (InvalidShippingOptionException) { + return back()->withErrors([ + 'shipping_option' => __('checkout.page.shipping_option_invalid'), + ]); + } + + return redirect()->route('checkout.show', $locale); + } +} diff --git a/database/seeders/CheckoutTranslationsSeeder.php b/database/seeders/CheckoutTranslationsSeeder.php new file mode 100644 index 0000000..9b76089 --- /dev/null +++ b/database/seeders/CheckoutTranslationsSeeder.php @@ -0,0 +1,118 @@ +lines() as $key => [$en, $el]) { + $exists = LanguageLine::query() + ->where('group', 'checkout') + ->where('key', $key) + ->exists(); + + if ($exists) { + $this->command?->warn("checkout.{$key} already exists — skipped"); + + continue; + } + + $translations->create('checkout', $key, ['en' => $en, 'el' => $el]); + $this->command?->info("checkout.{$key} added"); + } + } + + /** + * key => [English, Greek]. + * + * @return array + */ + private function lines(): array + { + return [ + // ── Cart drawer + order summary ────────────────────────────── + 'cart.title' => ['Your cart', 'Το καλάθι σου'], + 'cart.close' => ['Close', 'Κλείσιμο'], + 'cart.empty' => ['Your cart is empty', 'Το καλάθι σου είναι άδειο'], + 'cart.quantity' => ['Quantity', 'Ποσότητα'], + 'cart.increase' => ['Increase quantity', 'Αύξηση ποσότητας'], + 'cart.decrease' => ['Decrease quantity', 'Μείωση ποσότητας'], + 'cart.remove' => ['Remove', 'Αφαίρεση'], + 'cart.subtotal' => ['Subtotal', 'Υποσύνολο'], + 'cart.discount' => ['Discount', 'Έκπτωση'], + 'cart.total' => ['Total', 'Σύνολο'], + 'cart.checkout' => ['Checkout', 'Ολοκλήρωση παραγγελίας'], + 'cart.coupon_label' => ['Coupon code', 'Κωδικός κουπονιού'], + 'cart.coupon_placeholder' => ['Coupon code', 'Κωδικός κουπονιού'], + 'cart.coupon_apply' => ['Apply', 'Εφαρμογή'], + 'cart.coupon_remove' => ['Remove', 'Αφαίρεση'], + 'cart.coupon_invalid' => ["That coupon code isn't valid", 'Ο κωδικός κουπονιού δεν είναι έγκυρος'], + + // ── Checkout page ──────────────────────────────────────────── + 'page.title' => ['Checkout', 'Ολοκλήρωση παραγγελίας'], + 'page.contact_heading' => ['Contact', 'Στοιχεία επικοινωνίας'], + 'page.guest_tab' => ['Guest', 'Ως επισκέπτης'], + 'page.login_tab' => ['Log in', 'Σύνδεση'], + 'page.email_label' => ['Email', 'Email'], + 'page.marketing_consent' => [ + 'Email me offers, news and order reminders', + 'Στείλε μου προσφορές, νέα και υπενθυμίσεις παραγγελιών', + ], + 'page.login_email_label' => ['Email', 'Email'], + 'page.send_code' => ['Send code', 'Αποστολή κωδικού'], + 'page.login_coming_soon' => [ + 'Login is coming soon — continue as a guest for now.', + 'Η σύνδεση θα είναι διαθέσιμη σύντομα — προς το παρόν συνέχισε ως επισκέπτης.', + ], + 'page.billing_heading' => ['Billing information', 'Στοιχεία τιμολόγησης'], + 'page.shipping_heading' => ['Shipping information', 'Στοιχεία αποστολής'], + 'page.same_as_billing' => ['Same as billing address', 'Ίδια με τη διεύθυνση τιμολόγησης'], + 'page.first_name' => ['First name', 'Όνομα'], + 'page.last_name' => ['Last name', 'Επώνυμο'], + 'page.company_name' => ['Company name', 'Επωνυμία εταιρείας'], + 'page.tax_identifier' => ['Tax ID', 'ΑΦΜ'], + 'page.address_line_one' => ['Address', 'Διεύθυνση'], + 'page.address_line_two' => ['Address line 2', 'Διεύθυνση (γραμμή 2)'], + 'page.city' => ['City', 'Πόλη'], + 'page.state' => ['State / Region', 'Νομός / Περιοχή'], + 'page.postcode' => ['Postcode', 'Ταχυδρομικός κώδικας'], + 'page.country' => ['Country', 'Χώρα'], + 'page.country_placeholder' => ['Select a country', 'Επίλεξε χώρα'], + 'page.phone' => ['Phone', 'Τηλέφωνο'], + 'page.delivery_instructions' => ['Delivery notes', 'Σχόλια για την παράδοση'], + 'page.save_address' => ['Save and continue', 'Αποθήκευση και συνέχεια'], + 'page.shipping_method_heading' => ['Shipping method', 'Τρόπος αποστολής'], + 'page.shipping_method_empty' => [ + 'Add your shipping address to see delivery options.', + 'Συμπλήρωσε τη διεύθυνση αποστολής για να δεις τις διαθέσιμες επιλογές.', + ], + 'page.select_shipping_method' => ['Continue', 'Συνέχεια'], + 'page.shipping_option_invalid' => [ + 'That shipping option is no longer available.', + 'Αυτός ο τρόπος αποστολής δεν είναι πλέον διαθέσιμος.', + ], + 'page.continue_to_payment' => ['Continue to payment', 'Συνέχεια στην πληρωμή'], + 'page.order_summary_heading' => ['Order summary', 'Σύνοψη παραγγελίας'], + ]; + } +} diff --git a/resources/css/checkout.css b/resources/css/checkout.css index c1b7f12..a716c30 100644 --- a/resources/css/checkout.css +++ b/resources/css/checkout.css @@ -382,3 +382,246 @@ .bbk-cart-empty { 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 tabs */ + +.bbk-checkout-tabs { display: flex; flex-direction: column; gap: 1rem; } + +.bbk-checkout-tab { + display: inline-flex; + width: fit-content; + margin-right: 0.5rem; + padding: 0.5rem 1rem; + border: 1px solid var(--bbk-color-border); + border-radius: var(--bbk-radius-sm); + background: var(--bbk-color-bg); + font: inherit; + font-weight: 600; + color: var(--bbk-color-muted); + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; +} + +.bbk-checkout-tab[aria-selected="true"] { + background: var(--bbk-color-text); + border-color: var(--bbk-color-text); + color: var(--bbk-color-bg); +} + +.bbk-checkout-tab:focus-visible { + outline: 2px solid var(--bbk-color-accent); + outline-offset: 2px; +} + +/* 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); +} + +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. */ +.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; } + +/* 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; + 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; } diff --git a/resources/js/checkout/bbk-checkout-form-controller.js b/resources/js/checkout/bbk-checkout-form-controller.js new file mode 100644 index 0000000..a21edc7 --- /dev/null +++ b/resources/js/checkout/bbk-checkout-form-controller.js @@ -0,0 +1,46 @@ +import { Controller } from '@hotwired/stimulus' + +// Static, client-only interactivity for the checkout page's address form — no +// fetch, no cart mutation, just show/hide. Nothing here talks to the server; +// CheckoutController::saveAddress() is what actually persists anything. +export default class extends Controller { + static targets = [ + 'guestTab', 'loginTab', 'guestPanel', 'loginPanel', + 'sameAsBilling', 'shippingFields', + ] + + connect() { + // Reflect whatever the server rendered (old input on a validation + // redisplay, or the default) before any click happens. + if (this.hasSameAsBillingTarget) this.syncShippingFields() + } + + showGuest() { + this.guestPanelTarget.hidden = false + this.loginPanelTarget.hidden = true + this.guestTabTarget.setAttribute('aria-selected', 'true') + this.loginTabTarget.setAttribute('aria-selected', 'false') + } + + showLogin() { + this.guestPanelTarget.hidden = true + this.loginPanelTarget.hidden = false + this.guestTabTarget.setAttribute('aria-selected', 'false') + this.loginTabTarget.setAttribute('aria-selected', 'true') + } + + toggleSameAsBilling() { + this.syncShippingFields() + } + + // Disabled fields are simply never submitted by the browser — the server + // never sees stale shipping values while "same as billing" is checked. + syncShippingFields() { + const sameAsBilling = this.sameAsBillingTarget.checked + + this.shippingFieldsTarget.hidden = sameAsBilling + this.shippingFieldsTarget.querySelectorAll('input, select, textarea').forEach((field) => { + field.disabled = sameAsBilling + }) + } +} diff --git a/resources/js/checkout/index.js b/resources/js/checkout/index.js index 15f2df0..4db42d2 100644 --- a/resources/js/checkout/index.js +++ b/resources/js/checkout/index.js @@ -1,5 +1,6 @@ import BbkAddToCartController from './bbk-add-to-cart-controller' import BbkCartController from './bbk-cart-controller' +import BbkCheckoutFormController from './bbk-checkout-form-controller' // Registers the checkout module's Stimulus controllers onto the host app's // Stimulus application. Call once from the host's JS entry point: @@ -12,4 +13,5 @@ import BbkCartController from './bbk-cart-controller' export function registerCheckout(application) { application.register('bbk-add-to-cart', BbkAddToCartController) application.register('bbk-cart', BbkCartController) + application.register('bbk-checkout-form', BbkCheckoutFormController) } diff --git a/resources/views/checkout/components/field.blade.php b/resources/views/checkout/components/field.blade.php new file mode 100644 index 0000000..200283c --- /dev/null +++ b/resources/views/checkout/components/field.blade.php @@ -0,0 +1,30 @@ +{{-- + + + Generic labelled text input with old-input repopulation and validation + error display — the module's own equivalent of a host x-ui.field, used + instead of it per the module's independence rule. All styling is .bbk-field* + (resources/css/checkout.css); no host classes. +--}} +@props([ + 'name', + 'label', + 'type' => 'text', + 'value' => null, + 'required' => false, +]) + +
+ + class(['bbk-field-input', 'bbk-field-input--error' => $errors->has($name)]) }} + > + @error($name) +

{{ $message }}

+ @enderror +
diff --git a/resources/views/checkout/components/select.blade.php b/resources/views/checkout/components/select.blade.php new file mode 100644 index 0000000..3671594 --- /dev/null +++ b/resources/views/checkout/components/select.blade.php @@ -0,0 +1,38 @@ +{{-- + + + `options` is an iterable of {id, name} (a Collection of models works + directly) — value/label pulled from those two keys. +--}} +@props([ + 'name', + 'label', + 'options' => [], + 'value' => null, + 'placeholder' => null, + 'required' => false, +]) + +@php($selected = old($name, $value)) + +
+ + + @error($name) +

{{ $message }}

+ @enderror +
diff --git a/resources/views/checkout/components/textarea.blade.php b/resources/views/checkout/components/textarea.blade.php new file mode 100644 index 0000000..4d1c257 --- /dev/null +++ b/resources/views/checkout/components/textarea.blade.php @@ -0,0 +1,20 @@ +@props([ + 'name', + 'label', + 'value' => null, + 'required' => false, +]) + +
+ + + @error($name) +

{{ $message }}

+ @enderror +
diff --git a/resources/views/checkout/page.blade.php b/resources/views/checkout/page.blade.php new file mode 100644 index 0000000..4f19b48 --- /dev/null +++ b/resources/views/checkout/page.blade.php @@ -0,0 +1,250 @@ +{{-- + The checkout page. Two columns: left is contact + billing + shipping + + shipping method, right is the order summary (the same cart-body partial the + drawer uses, minus its own "Checkout" CTA — see .bbk-checkout-summary in + checkout.css). Stops short of payment for this slice — see + CheckoutController's class docblock. + + $cart, $lines, $billingAddress, $shippingAddress, $shippingOptions, + $countries come from CheckoutController::show(). +--}} +@extends('layouts.app') + +@section('title', __('checkout.page.title') . ' — ' . config('app.name')) + +@section('content') +
+

{{ __('checkout.page.title') }}

+ +
+
+ + {{-- Contact --}} +
+
+ + + + +
+ + + {{-- One combined marketing opt-in (offers + news + unfinished-order + reminders). Optional, unticked, never required — all of it is + direct marketing under ePrivacy (GR L. 3471/2006 art. 11), so it + needs an explicit opt-in and checkout can't be gated on it. + Persistence + prefill from the cart is Task 2 (back-end); + data_get() here already reads it once that lands. --}} + +
+ + {{-- Not wired yet — Modules\Core\Auth\Services\UserOtpService exists + (email + one-time code, passwordless) but nothing in the storefront + calls it yet. UI placeholder only; see project notes. --}} + +
+
+ +
+ @csrf + + {{-- Billing --}} +
+

{{ __('checkout.page.billing_heading') }}

+ +
+ + +
+ +
+ + +
+ + + + +
+ + +
+ +
+ + +
+ + +
+ + {{-- Shipping --}} +
+

{{ __('checkout.page.shipping_heading') }}

+ + + +
+
+ + +
+ + + + + + +
+ + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + {{-- Shipping method — only resolvable once a shipping address exists --}} +
+

{{ __('checkout.page.shipping_method_heading') }}

+ + @if ($shippingOptions->isEmpty()) +

{{ __('checkout.page.shipping_method_empty') }}

+ @else +
+ @csrf +
+ @foreach ($shippingOptions as $option) + + @endforeach +
+ @error('shipping_option') +

{{ $message }}

+ @enderror + + +
+ @endif +
+ + {{-- TODO: payment — next slice. boboko-core's Offline driver is the only + one fully wired end-to-end today; Stripe needs its client_secret / + 3-D Secure continuation handled before it can drive a real form here. --}} + + +
+ + +
+
+@endsection diff --git a/resources/views/checkout/partials/cart-body.blade.php b/resources/views/checkout/partials/cart-body.blade.php index 9fce2e5..4bcee8b 100644 --- a/resources/views/checkout/partials/cart-body.blade.php +++ b/resources/views/checkout/partials/cart-body.blade.php @@ -92,11 +92,9 @@ class="bbk-cart-coupon-input" {{ $cart?->total?->formatted() }} - {{-- TODO: point at the checkout page once that slice exists — - the drawer is the cart, there's no cart page for this to fall back to. --}} - + @endif diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 10e9222..125e034 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -52,7 +52,13 @@ - @include('checkout::drawer') + {{-- Not on the checkout page itself — its order-summary column already + plays the drawer's role there, and both reacting to the same + `bbk-cart:changed` event would pop the (redundant) drawer open over + the page every time a line's quantity changes in the summary. --}} + @unless (request()->routeIs('checkout.show')) + @include('checkout::drawer') + @endunless
@yield('content') diff --git a/routes/checkout.php b/routes/checkout.php index b4cfbbd..6039049 100644 --- a/routes/checkout.php +++ b/routes/checkout.php @@ -1,6 +1,7 @@ middleware('locale') ->group(function () { // No standalone cart page — the drawer (checkout::drawer) is the cart. - // The checkout page itself will live here once that slice is built. + Route::get('checkout', [CheckoutController::class, 'show']) + ->name('checkout.show'); + + Route::post('checkout/address', [CheckoutController::class, 'saveAddress']) + ->name('checkout.address.save'); + + Route::post('checkout/shipping-option', [CheckoutController::class, 'selectShippingOption']) + ->name('checkout.shipping-option.select'); + Route::post('cart/lines', [CartController::class, 'add']) ->name('checkout.cart.add');