generated from boboko/starter
add to cart button disabled when 0 stock and newsletter form theming
This commit is contained in:
@@ -109,6 +109,9 @@ private function buildOptionPicker(int $productId): array
|
|||||||
'id' => $variant->id,
|
'id' => $variant->id,
|
||||||
'price' => $variant->prices->first()?->price?->decimal(),
|
'price' => $variant->prices->first()?->price?->decimal(),
|
||||||
'image' => $variant->images->first()?->getUrl(),
|
'image' => $variant->images->first()?->getUrl(),
|
||||||
|
// Live from the DB (not the index's in_stock), with Lunar's own
|
||||||
|
// purchasability rule — drives the disabled add-to-cart button.
|
||||||
|
'inStock' => $variant->canBeFulfilledAtQuantity(1),
|
||||||
// handle => selected value id, for matching a combination of
|
// handle => selected value id, for matching a combination of
|
||||||
// selections back to this variant — see selectVariant() in
|
// selections back to this variant — see selectVariant() in
|
||||||
// product-form-controller.js.
|
// product-form-controller.js.
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ @layer components {
|
|||||||
transition: transform 0.35s cubic-bezier(0.2, 0.78, 0.12, 0.86);
|
transition: transform 0.35s cubic-bezier(0.2, 0.78, 0.12, 0.86);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover::after {
|
.btn-primary:not(:disabled):hover::after {
|
||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Controller } from '@hotwired/stimulus'
|
|||||||
import { formatPrice } from '../utils/format-price'
|
import { formatPrice } from '../utils/format-price'
|
||||||
|
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = ['price', 'image', 'swatch', 'colorName', 'stockError']
|
static targets = ['price', 'image', 'swatch', 'colorName', 'stockError', 'submit']
|
||||||
static values = {
|
static values = {
|
||||||
variants: Array,
|
variants: Array,
|
||||||
selected: Number,
|
selected: Number,
|
||||||
@@ -90,7 +90,7 @@ export default class extends Controller {
|
|||||||
// own server-side check have the final word rather than
|
// own server-side check have the final word rather than
|
||||||
// silently blocking the shopper here.
|
// silently blocking the shopper here.
|
||||||
} finally {
|
} finally {
|
||||||
if (submit) submit.disabled = false
|
if (submit) submit.disabled = !this.selectedVariant?.inStock
|
||||||
}
|
}
|
||||||
|
|
||||||
form.dataset.bbkStockChecked = 'true'
|
form.dataset.bbkStockChecked = 'true'
|
||||||
@@ -111,6 +111,10 @@ export default class extends Controller {
|
|||||||
this.stockErrorTarget.hidden = true
|
this.stockErrorTarget.hidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get selectedVariant() {
|
||||||
|
return this.variantsValue.find(v => v.id === this.selectedValue)
|
||||||
|
}
|
||||||
|
|
||||||
selectVariant(event) {
|
selectVariant(event) {
|
||||||
const option = event.currentTarget.dataset.option
|
const option = event.currentTarget.dataset.option
|
||||||
const valueId = parseInt(event.currentTarget.dataset.valueId)
|
const valueId = parseInt(event.currentTarget.dataset.valueId)
|
||||||
@@ -156,6 +160,9 @@ export default class extends Controller {
|
|||||||
const purchasableInput = this.element.querySelector('[data-bbk-purchasable-input]')
|
const purchasableInput = this.element.querySelector('[data-bbk-purchasable-input]')
|
||||||
if (purchasableInput) purchasableInput.value = id
|
if (purchasableInput) purchasableInput.value = id
|
||||||
|
|
||||||
|
// Out-of-stock variant (as of page load) can't be added at all.
|
||||||
|
if (this.hasSubmitTarget) this.submitTarget.disabled = !variant.inStock
|
||||||
|
|
||||||
this.swatchTargets.forEach(swatch => {
|
this.swatchTargets.forEach(swatch => {
|
||||||
const isSelected = this.selections[swatch.dataset.option] === parseInt(swatch.dataset.valueId)
|
const isSelected = this.selections[swatch.dataset.option] === parseInt(swatch.dataset.valueId)
|
||||||
swatch.classList.toggle('is-selected', isSelected)
|
swatch.classList.toggle('is-selected', isSelected)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
<section {{ $attributes }}>
|
<section {{ $attributes }}>
|
||||||
<div class="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-2 border border-black">
|
<div class="grid grid-cols-1 lg:grid-cols-2">
|
||||||
<div class="flex flex-col justify-center gap-8 px-8 py-16 lg:px-16">
|
<div class="flex flex-col justify-center gap-8 px-8 py-16 lg:px-16 items-center">
|
||||||
<h2
|
<h2
|
||||||
class="text-h2 leading-tight"
|
class="text-h1 leading-[1.3] max-w-xl text-center"
|
||||||
aria-label="Γράψου στο newsletter μας"
|
aria-label="Γράψου στο newsletter μας"
|
||||||
data-controller="appear"
|
data-controller="appear"
|
||||||
data-appear-visible-class="is-visible"
|
data-appear-visible-class="is-visible"
|
||||||
@@ -17,13 +17,13 @@ class="text-h2 leading-tight"
|
|||||||
μας
|
μας
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="max-w-md text-neutral-600">
|
<p class="max-w-xl text-neutral-600 text-center">
|
||||||
Γίνε μέλος της λίστας μας και κέρδισε <span class="font-bold">5% έκπτωση</span> στην πρώτη σου παραγγελία, μαζί με αποκλειστικές προσφορές και νέα.
|
Γίνε μέλος της λίστας μας και κέρδισε <span class="font-bold">5% έκπτωση</span> στην πρώτη σου παραγγελία, μαζί με αποκλειστικές προσφορές και νέα.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form method="POST" action="#" class="flex flex-col gap-6 max-w-md">
|
<form method="POST" action="#" class="flex flex-col gap-6 max-w-xl items-center">
|
||||||
@csrf
|
@csrf
|
||||||
<div class="relative">
|
<div class="relative max-w-xs w-full">
|
||||||
<x-ui.input
|
<x-ui.input
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
@@ -44,7 +44,7 @@ class="absolute right-0 bottom-2 cursor-pointer"
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-ui.checkbox name="gdpr_consent" :required="true" id="newsletter_split_gdpr_consent" class="after:mix-blend-multiply">
|
<x-ui.checkbox name="gdpr_consent" :required="true" id="newsletter_split_gdpr_consent" class="after:mix-blend-multiply">
|
||||||
<span class="text-sm text-left">Επιθυμώ διακαώς 🔥 να εγγραφώ στη λίστα αποστολής ενημερωτικού υλικού</span>
|
<span class="text-sm">Επιθυμώ διακαώς 🔥 να εγγραφώ στη λίστα αποστολής ενημερωτικού υλικού</span>
|
||||||
</x-ui.checkbox>
|
</x-ui.checkbox>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
// "absolute" via the class prop — Tailwind's generated stylesheet always
|
// "absolute" via the class prop — Tailwind's generated stylesheet always
|
||||||
// orders the "relative" utility after "absolute", so on a class clash
|
// orders the "relative" utility after "absolute", so on a class clash
|
||||||
// "relative" silently wins and the button never actually gets positioned.
|
// "relative" silently wins and the button never actually gets positioned.
|
||||||
$class = trim($position.' isolate inline-flex items-center justify-center border border-black text-black cursor-pointer no-underline '.$variantClasses.' '.$sizeClasses);
|
$class = trim($position.' isolate inline-flex items-center justify-center border border-black text-black cursor-pointer no-underline disabled:cursor-not-allowed disabled:opacity-50 '.$variantClasses.' '.$sizeClasses);
|
||||||
|
|
||||||
$attrs = $href
|
$attrs = $href
|
||||||
? $attributes->merge(['href' => $href, 'class' => $class])
|
? $attributes->merge(['href' => $href, 'class' => $class])
|
||||||
|
|||||||
@@ -1,13 +1,43 @@
|
|||||||
{{--
|
{{--
|
||||||
@include('emails.partials.button', ['url' => ..., 'label' => ...])
|
@include('emails.partials.button', ['url' => ..., 'label' => ...])
|
||||||
|
|
||||||
Bulletproof button: the padding sits on the link (whole area clickable)
|
The site's primary button (x-ui.button, .btn-primary): neutral-200 fill,
|
||||||
with mso-padding-alt on the cell so Outlook keeps the same size.
|
black border, bold italic uppercase, black offset shadow. The shadow is
|
||||||
|
built from table cells, not box-shadow (Gmail and Outlook drop it): a
|
||||||
|
black column on the right starting $offset down, and a black row at the
|
||||||
|
bottom starting $offset in. The button cell spans the first two rows, so
|
||||||
|
the black column always matches its height.
|
||||||
|
|
||||||
|
Uppercased here with the tonos removed, since emails can't run the site's
|
||||||
|
strip-accents script and CSS uppercase would keep the accents.
|
||||||
--}}
|
--}}
|
||||||
<table role="presentation" cellpadding="0" cellspacing="0" style="margin:0;">
|
@php
|
||||||
|
$offset = 8;
|
||||||
|
$label = strtr(mb_strtoupper($label), [
|
||||||
|
'Ά' => 'Α', 'Έ' => 'Ε', 'Ή' => 'Η', 'Ί' => 'Ι', 'Ό' => 'Ο', 'Ύ' => 'Υ', 'Ώ' => 'Ω',
|
||||||
|
'ΐ' => 'Ϊ', 'ΰ' => 'Ϋ',
|
||||||
|
]);
|
||||||
|
$spacer = "font-size:1px;line-height:1px;mso-line-height-rule:exactly;";
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:0;border-collapse:separate;">
|
||||||
<tr>
|
<tr>
|
||||||
<td bgcolor="#18c28a" style="background-color:#18c28a;border:1px solid #000000;border-radius:0;mso-padding-alt:14px 28px;">
|
<td rowspan="2" bgcolor="#e5e5e5" style="background-color:#e5e5e5;border:1px solid #000000;border-radius:0;mso-padding-alt:14px 28px;">
|
||||||
<a href="{{ $url }}" target="_blank" style="display:inline-block;padding:14px 28px;font-family:'Manrope',Arial,Helvetica,sans-serif;font-size:16px;line-height:1.2;font-weight:700;color:#000000;text-decoration:none;">{{ $label }}</a>
|
<a href="{{ $url }}" target="_blank" style="display:inline-block;padding:14px 28px;font-family:'Manrope',Arial,Helvetica,sans-serif;font-size:16px;line-height:20px;mso-line-height-rule:exactly;font-weight:700;font-style:italic;color:#000000;text-decoration:none;">{{ $label }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td width="{{ $offset }}" height="{{ $offset }}" style="width:{{ $offset }}px;height:{{ $offset }}px;{{ $spacer }}"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="{{ $offset }}" bgcolor="#000000" style="width:{{ $offset }}px;background-color:#000000;{{ $spacer }}"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td height="{{ $offset }}" style="height:{{ $offset }}px;padding:0 0 0 {{ $offset }}px;{{ $spacer }}">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td height="{{ $offset }}" bgcolor="#000000" style="height:{{ $offset }}px;background-color:#000000;{{ $spacer }}"> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td width="{{ $offset }}" height="{{ $offset }}" bgcolor="#000000" style="width:{{ $offset }}px;height:{{ $offset }}px;background-color:#000000;{{ $spacer }}"> </td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -195,7 +195,5 @@ class="max-w-xs"
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-20">
|
|
||||||
<x-newsletter-split />
|
<x-newsletter-split />
|
||||||
</div>
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -196,8 +196,13 @@ class="underline-slide font-semibold whitespace-nowrap"
|
|||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
|
@php
|
||||||
|
// Same initial pick as product-form-controller.js's connect()
|
||||||
|
// (?variant= or the first), so the button doesn't flip on load.
|
||||||
|
$initialVariant = collect($variantsData)->firstWhere('id', (int) request('variant')) ?? ($variantsData[0] ?? null);
|
||||||
|
@endphp
|
||||||
<x-checkout::add-to-cart
|
<x-checkout::add-to-cart
|
||||||
:purchasable="$variantsData[0]['id'] ?? null"
|
:purchasable="$initialVariant['id'] ?? null"
|
||||||
:quantity="false"
|
:quantity="false"
|
||||||
class="flex flex-col gap-6"
|
class="flex flex-col gap-6"
|
||||||
>
|
>
|
||||||
@@ -207,7 +212,7 @@ class="flex flex-col gap-6"
|
|||||||
|
|
||||||
<div class="flex items-stretch gap-10">
|
<div class="flex items-stretch gap-10">
|
||||||
<x-ui.quantity name="quantity" />
|
<x-ui.quantity name="quantity" />
|
||||||
<x-ui.button type="submit" class="flex-1">{{ __('storefront.product.add_to_cart') }}</x-ui.button>
|
<x-ui.button type="submit" class="flex-1" data-product-form-target="submit" :disabled="! ($initialVariant['inStock'] ?? true)">{{ __('storefront.product.add_to_cart') }}</x-ui.button>
|
||||||
</div>
|
</div>
|
||||||
</x-checkout::add-to-cart>
|
</x-checkout::add-to-cart>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user