feat: product screenshots gallery

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Doar Mihai
2026-10-17 16:00:38 +03:00
co-authored by Claude Sonnet 5.5
parent bf365d029f
commit a3894d2f37
9 changed files with 314 additions and 2 deletions
@@ -0,0 +1,43 @@
@props(['product'])
@php
$shots = $product->screenshots->values();
$urls = $shots->map->url->all();
$tiles = $shots->slice(1, 3)->values();
$more = max(0, $shots->count() - 4);
@endphp
@if($shots->isNotEmpty())
<div data-gallery
x-data="{ open: false, index: 0, urls: @js($urls),
show(i) { this.index = i; this.open = true },
next() { this.index = (this.index + 1) % this.urls.length },
prev() { this.index = (this.index - 1 + this.urls.length) % this.urls.length } }"
@keydown.escape.window="open = false"
@keydown.arrow-right.window="open && next()"
@keydown.arrow-left.window="open && prev()"
{{ $attributes }}>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
<button type="button" @click="show(0)" class="sm:col-span-2 sm:row-span-3 block overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<img src="{{ $urls[0] }}" alt="{{ $product->title }} screenshot 1" class="w-full h-full object-cover aspect-video sm:aspect-auto">
</button>
@foreach($tiles as $i => $shot)
<button type="button" @click="show({{ $i + 1 }})" class="relative block overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<img src="{{ $shot->url }}" alt="{{ $product->title }} screenshot {{ $i + 2 }}" class="w-full h-full object-cover aspect-video">
@if($loop->last && $more > 0)
<span class="absolute inset-0 flex items-center justify-center bg-black/50 text-white font-semibold">+{{ $more }} more</span>
@endif
</button>
@endforeach
</div>
<div x-show="open" style="display: none" x-transition.opacity
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
@click.self="open = false" role="dialog" aria-modal="true">
<button type="button" @click="open = false" class="absolute top-4 right-4 text-white text-3xl leading-none" aria-label="Close">&times;</button>
<button type="button" x-show="urls.length > 1" @click="prev()" class="absolute left-4 text-white text-4xl" aria-label="Previous">&lsaquo;</button>
<img :src="urls[index]" alt="" class="max-h-full max-w-full object-contain">
<button type="button" x-show="urls.length > 1" @click="next()" class="absolute right-4 text-white text-4xl" aria-label="Next">&rsaquo;</button>
</div>
</div>
@endif
+28
View File
@@ -179,6 +179,34 @@
</div>
</div>
<!-- Screenshots -->
<div x-data="{ previews: [] }">
<label for="screenshots" class="block text-sm font-medium text-gray-700">
Screenshots
</label>
<div class="mt-1">
<input type="file"
id="screenshots"
name="screenshots[]"
accept="image/*"
multiple
@change="previews = Array.from($event.target.files).map(f => URL.createObjectURL(f))"
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100">
@error('screenshots')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
@foreach($errors->get('screenshots.*') as $messages)
<p class="mt-2 text-sm text-red-600">{{ $messages[0] }}</p>
@endforeach
<p class="mt-2 text-sm text-gray-500">Add several images (JPEG, PNG, or GIF, max 2MB each).</p>
<div class="mt-3 flex flex-wrap gap-2" x-show="previews.length" style="display: none">
<template x-for="src in previews" :key="src">
<img :src="src" alt="" class="h-20 w-28 object-cover rounded border border-gray-300">
</template>
</div>
</div>
</div>
<!-- Info Alert -->
<div class="rounded-md bg-blue-50 p-4">
<div class="flex">
+46
View File
@@ -205,6 +205,52 @@
</div>
</div>
@if($product->screenshots->isNotEmpty())
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Current Screenshots:</label>
<div class="mt-1 grid grid-cols-2 sm:grid-cols-4 gap-3">
@foreach($product->screenshots as $shot)
<label class="block">
<img src="{{ $shot->url }}" alt="" class="h-24 w-full object-cover rounded border border-gray-300">
<span class="mt-1 flex items-center text-sm text-gray-700">
<input type="checkbox" name="remove_screenshots[]" value="{{ $shot->id }}"
class="h-4 w-4 text-primary-600 border-gray-300 rounded">
<span class="ml-2">Remove</span>
</span>
</label>
@endforeach
</div>
</div>
@endif
<!-- Screenshots -->
<div x-data="{ previews: [] }">
<label for="screenshots" class="block text-sm font-medium text-gray-700">
Screenshots
</label>
<div class="mt-1">
<input type="file"
id="screenshots"
name="screenshots[]"
accept="image/*"
multiple
@change="previews = Array.from($event.target.files).map(f => URL.createObjectURL(f))"
class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100">
@error('screenshots')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
@foreach($errors->get('screenshots.*') as $messages)
<p class="mt-2 text-sm text-red-600">{{ $messages[0] }}</p>
@endforeach
<p class="mt-2 text-sm text-gray-500">Add several images (JPEG, PNG, or GIF, max 2MB each).</p>
<div class="mt-3 flex flex-wrap gap-2" x-show="previews.length" style="display: none">
<template x-for="src in previews" :key="src">
<img :src="src" alt="" class="h-20 w-28 object-cover rounded border border-gray-300">
</template>
</div>
</div>
</div>
<!-- Info Alert -->
<div class="rounded-md bg-blue-50 p-4">
<div class="flex">
+1 -1
View File
@@ -118,7 +118,7 @@
</div>
<!-- Gallery -->
<!-- slot:gallery -->
<x-product.gallery :product="$product" class="mt-6" />
<div class="mt-6 grid grid-cols-1 lg:grid-cols-4 gap-6">
<!-- Main column -->