product custom fields

This commit is contained in:
elvira
2026-09-23 17:53:23 +03:00
parent a4240b6474
commit 580adac33a
24 changed files with 670 additions and 12 deletions
@@ -0,0 +1,76 @@
{{--
<x-product-custom-fields :fields="$product['custom_fields']" />
The product's custom fields (boboko-core's Product::$custom_fields), rendered
inside the add-to-cart <form> so their values travel with it as
custom_fields[key]. text → input, textarea → textarea, file → photo upload.
A photo is uploaded as soon as it's picked (custom-field-upload-controller.js)
and only the returned reference is submitted with the form — the file input
itself has no name. It still carries `required`, so the browser's own
validation blocks add-to-cart until a photo is picked, and the controller
marks it invalid (setCustomValidity) while the upload is in flight.
--}}
@props(['fields' => []])
@use('App\Http\Controllers\CustomFieldUploadController')
<div class="flex flex-col gap-6">
@foreach ($fields as $field)
@php
$id = 'custom-field-'.$field['key'];
$name = 'custom_fields['.$field['key'].']';
$required = (bool) ($field['required'] ?? false);
@endphp
@switch($field['type'])
@case('textarea')
<x-ui.field :label="$field['label']" :for="$id" :required="$required">
<x-ui.textarea :id="$id" :name="$name" :required="$required" :rows="4" maxlength="2000" />
</x-ui.field>
@break
@case('file')
<x-ui.field
:label="$field['label']"
:for="$id"
:required="$required"
:description="__('storefront.product.custom_field_photo_hint', ['size' => CustomFieldUploadController::MAX_KILOBYTES / 1024])"
data-controller="custom-field-upload"
data-custom-field-upload-url-value="{{ route('custom-field-upload.store') }}"
data-custom-field-upload-label-value="{{ $field['label'] }}"
data-custom-field-upload-uploading-message-value="{{ __('storefront.product.custom_field_uploading') }}"
data-custom-field-upload-failed-message-value="{{ __('storefront.product.custom_field_upload_failed') }}"
>
<input type="hidden" name="{{ $name }}" data-custom-field-upload-target="reference">
<x-ui.file-input
:id="$id"
:accept="CustomFieldUploadController::accept()"
:required="$required"
aria-describedby="{{ $id }}-description {{ $id }}-error"
data-custom-field-upload-target="file"
data-action="change->custom-field-upload#upload"
/>
<img
alt=""
width="96"
height="96"
hidden
class="w-24 h-24 object-cover border border-black"
data-custom-field-upload-target="preview"
data-action="error->custom-field-upload#hidePreview"
>
<p id="{{ $id }}-error" class="text-sm text-red-600" role="alert" hidden data-custom-field-upload-target="error"></p>
</x-ui.field>
@break
@default
<x-ui.field :label="$field['label']" :for="$id" :required="$required">
<x-ui.input :id="$id" :name="$name" :required="$required" maxlength="255" />
</x-ui.field>
@endswitch
@endforeach
</div>
@@ -25,6 +25,7 @@
:image="$product['image'] ?? null"
:href="$product['href'] ?? '#'"
:variant-id="$product['variantId'] ?? null"
:has-custom-fields="$product['hasCustomFields'] ?? false"
/>
@endforeach
</div>
@@ -0,0 +1,20 @@
@props([
'accept' => null,
'required' => false,
'disabled' => false,
])
<input
type="file"
@if ($accept) accept="{{ $accept }}" @endif
@required($required)
@disabled($disabled)
{{ $attributes->merge([
'class' => 'w-full py-2 text-sm
file:mr-4 file:py-2 file:px-4 file:rounded-none file:border file:border-black file:bg-transparent
file:font-semibold file:cursor-pointer file:transition-colors
hover:file:bg-black hover:file:text-neutral-200
focus:outline-none focus-visible:ring-2 focus-visible:ring-black
disabled:cursor-not-allowed disabled:opacity-50',
]) }}
/>
@@ -4,6 +4,7 @@
'image' => null,
'href' => '#',
'variantId' => null,
'hasCustomFields' => false,
])
{{-- data-turbo-frame="_top" on the links: this card renders inside the
@@ -28,7 +29,18 @@ class="w-full h-auto block"
@endif
</a>
@if ($variantId)
@if ($hasCustomFields)
{{-- Custom fields have to be filled in on the product page. --}}
<x-ui.button
:href="$href"
size="md"
position="absolute"
data-turbo-frame="_top"
class="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 transition-opacity duration-100"
>
{{ __('storefront.product.personalize') }}
</x-ui.button>
@elseif ($variantId)
{{-- has-[...] forces the button visible while an add-to-cart error
is showing, so it isn't only readable on hover — a shopper who
already moved off the card (mouse or the click itself) must