Skip to main content

Black Friday 2025! Only until December 1st: coupon FRIDAY25 for 40% off Yearly/Lifetime membership!

Read more here

lunarphp/livewire-starter-kit

194 stars
2 code files
View lunarphp/livewire-starter-kit on GitHub

app/Livewire/ProductPage.php

Open in GitHub
use App\Traits\FetchesUrls;
use Illuminate\Support\Collection;
use Illuminate\View\View;
use Livewire\Component;
use Lunar\Models\Product;
use Lunar\Models\ProductVariant;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
 
class ProductPage extends Component
{
use FetchesUrls;
 
public array $selectedOptionValues = [];
 
public function mount($slug): void
{
$this->url = $this->fetchUrl(
$slug,
(new Product)->getMorphClass(),
[
'element.media',
'element.variants.basePrices.currency',
'element.variants.basePrices.priceable',
'element.variants.values.option',
]
);
 
$this->selectedOptionValues = $this->productOptions->mapWithKeys(function ($data) {
return [$data['option']->id => $data['values']->first()->id];
})->toArray();
 
if (! $this->variant) {
abort(404);
}
}
 
public function getVariantProperty(): ProductVariant
{
return $this->product->variants->first(function ($variant) {
return ! $variant->values->pluck('id')
->diff(
collect($this->selectedOptionValues)->values()
)->count();
});
}
 
public function getProductOptionValuesProperty(): Collection
{
return $this->product->variants->pluck('values')->flatten();
}
 
public function getProductOptionsProperty(): Collection
{
return $this->productOptionValues->unique('id')->groupBy('product_option_id')
->map(function ($values) {
return [
'option' => $values->first()->option,
'values' => $values,
];
})->values();
}
 
public function getProductProperty(): Product
{
return $this->url->element;
}
 
public function getImagesProperty(): Collection
{
return $this->product->media->sortBy('order_column');
}
 
public function getImageProperty(): ?Media
{
if (count($this->variant->images)) {
return $this->variant->images->first();
}
 
if ($primary = $this->images->first(fn ($media) => $media->getCustomProperty('primary'))) {
return $primary;
}
 
return $this->images->first();
}
 
public function render(): View
{
return view('livewire.product-page');
}
}

resources/views/livewire/product-page.blade.php

Open in GitHub
<section>
<div class="max-w-screen-xl px-4 py-12 mx-auto sm:px-6 lg:px-8">
<div class="grid items-start grid-cols-1 gap-8 md:grid-cols-2">
<div class="grid grid-cols-2 gap-4 md:grid-cols-1">
@if ($this->image)
<div class="aspect-w-1 aspect-h-1">
<img class="object-cover rounded-xl"
src="{{ $this->image->getUrl('large') }}"
alt="{{ $this->product->translateAttribute('name') }}" />
</div>
@endif
 
<div class="grid grid-cols-2 gap-4 sm:grid-cols-4">
@foreach ($this->images as $image)
<div class="aspect-w-1 aspect-h-1"
wire:key="image_{{ $image->id }}">
<img loading="lazy"
class="object-cover rounded-xl"
src="{{ $image->getUrl('small') }}"
alt="{{ $this->product->translateAttribute('name') }}" />
</div>
@endforeach
</div>
</div>
 
<div>
<div class="flex items-center justify-between">
<h1 class="text-xl font-bold">
{{ $this->product->translateAttribute('name') }}
</h1>
 
<x-product-price class="ml-4 font-medium"
:variant="$this->variant" />
</div>
 
<p class="mt-1 text-sm text-gray-500">
{{ $this->variant->sku }}
</p>
 
<article class="mt-4 text-gray-700">
{!! $this->product->translateAttribute('description') !!}
</article>
 
<form class="mt-4">
<div class="space-y-4">
@foreach ($this->productOptions as $option)
<fieldset>
<legend class="text-xs font-medium text-gray-700">
{{ $option['option']->translate('name') }}
</legend>
 
<div class="flex flex-wrap gap-2 mt-2 text-xs tracking-wide uppercase"
x-data="{
selectedOption: @entangle('selectedOptionValues').live,
selectedValues: [],
}"
x-init="selectedValues = Object.values(selectedOption);
$watch('selectedOption', value =>
selectedValues = Object.values(selectedOption)
)">
@foreach ($option['values'] as $value)
<button class="px-6 py-4 font-medium border rounded-lg focus:outline-none focus:ring"
type="button"
wire:click="
$set('selectedOptionValues.{{ $option['option']->id }}', {{ $value->id }})
"
:class="{
'bg-indigo-600 border-indigo-600 text-white hover:bg-indigo-700': selectedValues
.includes({{ $value->id }}),
'hover:bg-gray-100': !selectedValues.includes({{ $value->id }})
}">
{{ $value->translate('name') }}
</button>
@endforeach
</div>
</fieldset>
@endforeach
</div>
 
<div class="max-w-xs mt-8">
<livewire:components.add-to-cart :purchasable="$this->variant"
:wire:key="$this->variant->id">
</div>
</form>
</div>
</div>
</div>
</section>

We'd Love Your Feedback

Tell us what you like or what we can improve

Feel free to share anything you like or dislike about this page or the platform in general.