generated from boboko/starter
292 lines
15 KiB
PHP
292 lines
15 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', $product['name'] . ' — ' . config('app.name'))
|
|
@section('description', $product['description'])
|
|
|
|
@section('content')
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-8 py-12">
|
|
|
|
<x-breadcrumb class="mb-14 justify-end" :items="[
|
|
$collection
|
|
? ['label' => $collection['name'], 'href' => route('category.show', ['id' => $collection['id']])]
|
|
: ['label' => __('storefront.nav.products'), 'href' => route('products')],
|
|
['label' => $product['name']],
|
|
]" />
|
|
|
|
<div
|
|
class="grid grid-cols-1 md:grid-cols-2 gap-12"
|
|
data-controller="product-form"
|
|
data-product-form-variants-value="{{ json_encode($variantsData) }}"
|
|
data-product-form-stock-check-url-value="{{ route('product.stock-check', app()->getLocale()) }}"
|
|
data-product-form-stock-error-one-value="{{ trans_choice('storefront.product.add_to_cart_failed', 1) }}"
|
|
data-product-form-stock-error-many-value="{{ trans_choice('storefront.product.add_to_cart_failed', 2, ['count' => ':count']) }}"
|
|
>
|
|
|
|
{{-- Image --}}
|
|
{{-- relative: anchors the wishlist heart to the top right, which is
|
|
the main image's corner (thumbnails sit on the left). --}}
|
|
<div
|
|
data-controller="product-gallery"
|
|
class="relative flex gap-4 items-start"
|
|
>
|
|
<x-wishlist-button :product-id="$product['id']" class="absolute top-4 right-4 z-10" />
|
|
|
|
@if(!empty($product['media']))
|
|
{{-- Thumbnails --}}
|
|
<div class="flex flex-col items-center gap-1 w-[116px] shrink-0 -mt-12.5">
|
|
<button
|
|
type="button"
|
|
data-action="click->product-gallery#scrollUp"
|
|
data-product-gallery-target="arrow"
|
|
class="gallery-arrow invisible w-full flex items-center justify-center py-2"
|
|
aria-label="Scroll thumbnails up"
|
|
><x-ui.icon name="arrow-up" :size="30" /></button>
|
|
|
|
<div
|
|
data-product-gallery-target="track"
|
|
class="flex flex-col gap-4 overflow-hidden"
|
|
style="max-height: var(--gallery-height, 600px)"
|
|
>
|
|
@foreach($product['media'] as $i => $media)
|
|
<button
|
|
type="button"
|
|
data-action="click->product-gallery#select"
|
|
data-product-gallery-target="thumb"
|
|
data-src="{{ $media['url'] }}"
|
|
data-alt="{{ $product['name'] }}"
|
|
class="block w-full shrink-0 border border-black overflow-hidden "
|
|
aria-label="View image {{ $i + 1 }}"
|
|
aria-pressed="{{ $i === 0 ? 'true' : 'false' }}"
|
|
>
|
|
|
|
<!-- opacity-50 transition-opacity {{ $i === 0 ? 'opacity-100' : '' }}" -->
|
|
<img src="{{ $media['url'] }}" alt="" class="w-full h-auto object-cover" aria-hidden="true">
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
data-action="click->product-gallery#scrollDown"
|
|
data-product-gallery-target="arrow"
|
|
class="gallery-arrow invisible w-full flex items-center justify-center py-2"
|
|
aria-label="Scroll thumbnails down"
|
|
><x-ui.icon name="arrow-down" :size="30" /></button>
|
|
</div>
|
|
|
|
{{-- Main image --}}
|
|
<button
|
|
type="button"
|
|
class="flex-1 border border-black bg-white cursor-zoom-in block p-0"
|
|
data-action="click->product-gallery#openLightbox"
|
|
aria-label="View full size image"
|
|
>
|
|
<img
|
|
data-product-form-target="image"
|
|
data-product-gallery-target="main"
|
|
src="{{ $product['media'][0]['url'] }}"
|
|
alt="{{ $product['name'] }}"
|
|
class="w-full h-auto block"
|
|
>
|
|
</button>
|
|
|
|
{{-- Lightbox --}}
|
|
<div
|
|
id="product-lightbox"
|
|
popover
|
|
class="product-lightbox"
|
|
data-product-gallery-target="lightbox"
|
|
data-action="click->product-gallery#closeLightboxOnBackdrop"
|
|
role="dialog"
|
|
aria-label="Product image gallery"
|
|
aria-modal="true"
|
|
>
|
|
{{-- Prev --}}
|
|
<button
|
|
type="button"
|
|
data-action="click->product-gallery#prevImage click->product-gallery#noop"
|
|
class="lightbox-arrow absolute left-8 top-1/2 -translate-y-1/2"
|
|
aria-label="Previous image"
|
|
><x-ui.icon name="arrow-left" :size="72" color="white" /></button>
|
|
|
|
{{-- Image + close button as a unit --}}
|
|
<div class="relative" data-action="click->product-gallery#noop">
|
|
<img
|
|
data-product-gallery-target="lightboxImage"
|
|
src=""
|
|
alt=""
|
|
class="max-h-[90vh] max-w-[80vw] object-contain block"
|
|
>
|
|
<button
|
|
type="button"
|
|
popovertarget="product-lightbox"
|
|
popovertargetaction="hide"
|
|
class="lightbox-close absolute -top-10 -right-10"
|
|
aria-label="Close gallery"
|
|
><x-ui.icon name="close" :size="36" color="white" /></button>
|
|
</div>
|
|
|
|
{{-- Next --}}
|
|
<button
|
|
type="button"
|
|
data-action="click->product-gallery#nextImage click->product-gallery#noop"
|
|
class="lightbox-arrow absolute right-8 top-1/2 -translate-y-1/2"
|
|
aria-label="Next image"
|
|
><x-ui.icon name="arrow-right" :size="72" color="white" /></button>
|
|
|
|
{{-- Counter --}}
|
|
<p
|
|
data-product-gallery-target="lightboxCounter"
|
|
class="absolute bottom-6 right-8 text-white text-sm"
|
|
aria-live="polite"
|
|
></p>
|
|
</div>
|
|
@else
|
|
<div class="w-full bg-neutral-300 flex items-center justify-center text-neutral-500 min-h-64">
|
|
{{ __('storefront.product.no_image') }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Info --}}
|
|
<div class="flex flex-col gap-6">
|
|
|
|
<h1 class="font-display font-medium text-4xl lg:text-[54px]">
|
|
{{ $product['name'] }}
|
|
</h1>
|
|
|
|
<x-reviews-stars
|
|
:rating="$product['reviews']['average_rating'] ?? 0"
|
|
:count="$product['reviews']['count']"
|
|
:showCount="true"
|
|
:linkable="true"
|
|
data-controller="tab-link"
|
|
data-tab-link-tabs-outlet="#product-tabs"
|
|
data-tab-link-panel-value="reviews"
|
|
/>
|
|
|
|
@if($product['price'] !== null)
|
|
<p class="text-2xl font-bold" data-product-form-target="price">
|
|
<x-ui.price :amount="$product['price']" />
|
|
</p>
|
|
@endif
|
|
|
|
@php
|
|
$desc = html_entity_decode(strip_tags($product['description'] ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
$descTruncated = Str::limit($desc, 137);
|
|
$descNeedsMore = mb_strlen($desc) > mb_strlen(rtrim($descTruncated, '.'));
|
|
@endphp
|
|
<div class="text-base leading-relaxed">
|
|
{{ $descTruncated }}
|
|
@if($descNeedsMore)
|
|
<a
|
|
href="#tab-panel-description"
|
|
class="underline-slide font-semibold whitespace-nowrap"
|
|
data-controller="tab-link"
|
|
data-tab-link-tabs-outlet="#product-tabs"
|
|
data-tab-link-panel-value="description"
|
|
data-action="click->tab-link#activate"
|
|
>{{ __('storefront.product.read_more') }}</a>
|
|
@endif
|
|
</div>
|
|
|
|
@foreach($productOptions as $productOption)
|
|
@if($productOption['isColor'])
|
|
<x-ui.color-swatch :values="$productOption['values']" :option="$productOption['label']" :option-handle="$productOption['handle']" />
|
|
@else
|
|
<x-ui.option-buttons :values="$productOption['values']" :option="$productOption['label']" :option-handle="$productOption['handle']" />
|
|
@endif
|
|
@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
|
|
:purchasable="$initialVariant['id'] ?? null"
|
|
:quantity="false"
|
|
class="flex flex-col gap-6"
|
|
>
|
|
@if(!empty($product['custom_fields']))
|
|
<x-product-custom-fields :fields="$product['custom_fields']" />
|
|
@endif
|
|
|
|
<div class="flex items-stretch gap-10">
|
|
<x-ui.quantity name="quantity" />
|
|
<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>
|
|
</x-checkout::add-to-cart>
|
|
|
|
{{-- Storefront-owned, not part of the checkout module — the
|
|
local stock pre-check in product-form-controller.js stops
|
|
an over-limit submit before it ever reaches the module
|
|
and reports it here. --}}
|
|
<p class="text-sm text-red-600" data-product-form-target="stockError" hidden role="alert"></p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<x-ui.tabs id="product-tabs" class="mt-16 scroll-mt-28" size="lg" :active="request('tab')" :tabs="[
|
|
['id' => 'description', 'label' => __('storefront.product.description')],
|
|
['id' => 'reviews', 'label' => __('storefront.product.reviews') . ' (' . $product['reviews']['count'] . ')'],
|
|
]">
|
|
<x-slot name="description">
|
|
<div class="leading-7 [&_p]:mt-4 [&_ul]:flex [&_ul]:flex-col [&_ul]:gap-3 [&_ul]:text-neutral-600 [&_ul]:list-disc [&_ul]:list-outside [&_ul]:pl-5">
|
|
{!! $product['description'] !!}
|
|
</div>
|
|
</x-slot>
|
|
<x-slot name="reviews">
|
|
@if(session('reviewSubmitted'))
|
|
<p class="mb-8 text-sm font-semibold" role="status">{{ __('storefront.review.thank_you') }}</p>
|
|
@endif
|
|
|
|
@if(!empty($product['reviews']['items']))
|
|
<div class="mb-10">
|
|
@foreach($product['reviews']['items'] as $review)
|
|
<x-review-card :review="[
|
|
'rating' => $review['rating'],
|
|
'name' => $review['reviewer_name'],
|
|
'date' => $review['reviewed_at'] ? \Illuminate\Support\Carbon::createFromTimestamp($review['reviewed_at'])->translatedFormat('d M Y') : '',
|
|
'text' => $review['body'],
|
|
'image' => $review['media'][0]['url'] ?? null,
|
|
'reply' => $review['reply'] ?? null,
|
|
'replyDate' => $review['replied_at'] ? \Illuminate\Support\Carbon::createFromTimestamp($review['replied_at'])->translatedFormat('d M Y') : null,
|
|
]" />
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<p class="text-neutral-500 mb-10">{{ __('storefront.review.no_reviews_yet') }}</p>
|
|
@endif
|
|
|
|
<h3 class="text-h4 font-bold">
|
|
{{ $product['reviews']['count'] > 0 ? __('storefront.review.write_new') : __('storefront.review.write_first') }}
|
|
{{ __('storefront.review.for_product', ['name' => $product['name']]) }}
|
|
</h3>
|
|
|
|
<x-review-form :product="$product" />
|
|
</x-slot>
|
|
</x-ui.tabs>
|
|
|
|
@if(!empty($product['recommendations']))
|
|
<x-product-grid
|
|
class="mt-20"
|
|
:title="__('storefront.product.related')"
|
|
:products="collect($product['recommendations'])->map(fn (array $recommendation) => [
|
|
'name' => $recommendation['name'],
|
|
'price' => $recommendation['price'],
|
|
'image' => $recommendation['image'],
|
|
'href' => route('product.show', ['id' => $recommendation['id']]),
|
|
'variantId' => $recommendation['variant_id'] ?? null,
|
|
'hasCustomFields' => $recommendation['has_custom_fields'] ?? false,
|
|
])->all()"
|
|
/>
|
|
@endif
|
|
|
|
</div>
|
|
|
|
<x-newsletter class="py-20" />
|
|
|
|
@endsection
|