diff --git a/composer.json b/composer.json index 1d3bf23..bb07335 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "Modules\\Core\\Providers\\AuthServiceProvider", "Modules\\Core\\Providers\\CustomerServiceProvider", "Modules\\Core\\Providers\\CheckoutServiceProvider", + "Modules\\Core\\Providers\\CheckoutModuleServiceProvider", "Modules\\Core\\Providers\\PaymentServiceProvider", "Modules\\Core\\Providers\\LocalizationServiceProvider", "Modules\\Core\\Providers\\CatalogServiceProvider", diff --git a/config/checkout.php b/config/checkout.php new file mode 100644 index 0000000..22d571a --- /dev/null +++ b/config/checkout.php @@ -0,0 +1,44 @@ +`, so the + * login page must send the shopper back there afterwards. null: no login + * offered in checkout at all. + */ + 'login_route' => 'login', + + /* + * Name of the storefront's product-listing route — where confirmation() + * redirects a visit with no placed order to look at (session expired, + * direct navigation, a bookmark). route($this, $locale) must resolve. + */ + 'products_route' => 'products', + + /* + * ISO 3166-1 alpha-3 code fixing checkout to a single country (a hidden + * field, forced server-side — no country picker shown at all). null (the + * default) gives the full country/region picker, for a multi-country + * store. + */ + 'store_country_iso3' => null, + + /* + * The `purpose` tag CartController expects a product custom field's + * `file` answer to already carry (see Modules\Core\File\Models\File) — + * matches whatever purpose string the host's own upload endpoint + * (extending Modules\Core\File\Http\Controllers\UploadFileController) + * tags its stored files with. This module never reaches into that + * host controller directly; this config value is the one shared + * source of truth between the two. + */ + 'custom_field_upload_purpose' => 'custom-field-upload', + +]; diff --git a/resources/css/checkout.css b/resources/css/checkout.css new file mode 100644 index 0000000..51a4648 --- /dev/null +++ b/resources/css/checkout.css @@ -0,0 +1,856 @@ +/* + * 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); } + +/* 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 new file mode 100644 index 0000000..1f1cdf2 --- /dev/null +++ b/resources/js/checkout/bbk-add-to-cart-controller.js @@ -0,0 +1,57 @@ +import { Controller } from '@hotwired/stimulus' +import { csrfToken } from './csrf' + +// Sits on anhas($name)) hidden @endunless>{{ $errors->first($name) }}
+