Files
3dealer/resources/views/product/show.blade.php
T

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