diff --git a/Modules/Conversation/App/Models/Conversation.php b/Modules/Conversation/App/Models/Conversation.php index 2463d3689..2c5252420 100644 --- a/Modules/Conversation/App/Models/Conversation.php +++ b/Modules/Conversation/App/Models/Conversation.php @@ -187,7 +187,7 @@ class Conversation extends Model { $this->loadMissing('listing'); - $url = $this->listing?->getFirstMediaUrl('listing-images'); + $url = $this->listing?->primaryImageUrl('thumb', 'desktop'); return is_string($url) && trim($url) !== '' ? $url : null; } diff --git a/Modules/Conversation/resources/views/partials/inbox-list-pane.blade.php b/Modules/Conversation/resources/views/partials/inbox-list-pane.blade.php index 82e8909ff..1dd7b735b 100644 --- a/Modules/Conversation/resources/views/partials/inbox-list-pane.blade.php +++ b/Modules/Conversation/resources/views/partials/inbox-list-pane.blade.php @@ -19,14 +19,18 @@ $conversationListing = $conversation->listing; $partner = (int) $conversation->buyer_id === (int) auth()->id() ? $conversation->seller : $conversation->buyer; $isSelected = $selectedConversation && (int) $selectedConversation->id === (int) $conversation->id; - $conversationImage = $conversationListing?->getFirstMediaUrl('listing-images'); + $conversationImage = $conversationListing?->primaryImageData('thumb'); $lastMessage = trim((string) ($conversation->lastMessage?->body ?? '')); @endphp
@if($conversationImage) - {{ $conversationListing?->title }} + @include('listing::partials.responsive-image', [ + 'image' => $conversationImage, + 'alt' => $conversationListing?->title, + 'class' => 'w-full h-full object-cover', + ]) @else
Listing
@endif diff --git a/Modules/Favorite/resources/views/index.blade.php b/Modules/Favorite/resources/views/index.blade.php index ab5fa414e..1b4c53603 100644 --- a/Modules/Favorite/resources/views/index.blade.php +++ b/Modules/Favorite/resources/views/index.blade.php @@ -64,7 +64,7 @@ @forelse($favoriteListings as $listing) @php - $listingImage = $listing->getFirstMediaUrl('listing-images'); + $listingImage = $listing->primaryImageData('card'); $priceLabel = $listing->price ? number_format((float) $listing->price, 0).' '.$listing->currency : 'Free'; $meta = collect([ $listing->category?->name, @@ -80,7 +80,11 @@
@if($listingImage) - {{ $listing->title }} + @include('listing::partials.responsive-image', [ + 'image' => $listingImage, + 'alt' => $listing->title, + 'class' => 'w-full h-full object-cover', + ]) @else
No image
@endif diff --git a/Modules/Listing/Database/Seeders/ListingSeeder.php b/Modules/Listing/Database/Seeders/ListingSeeder.php index 7a8da61d2..12e2b52a1 100644 --- a/Modules/Listing/Database/Seeders/ListingSeeder.php +++ b/Modules/Listing/Database/Seeders/ListingSeeder.php @@ -8,7 +8,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use Modules\Category\Models\Category; use Modules\Listing\Models\Listing; -use Modules\Listing\Support\DemoListingImageFactory; +use Modules\Listing\Support\SampleListingImageCatalog; use Modules\Location\Models\City; use Modules\Location\Models\Country; use Modules\User\App\Models\User; @@ -143,7 +143,6 @@ class ListingSeeder extends Seeder $location = $this->resolveLocation($index, $countries, $turkeyCities); $title = $this->buildTitle($category, $index, $user); $slug = 'demo-'.Str::slug($user->email).'-'.$category->slug; - $familyName = trim((string) ($category->parent?->name ?? $category->name)); return [ 'slug' => $slug, @@ -156,13 +155,7 @@ class ListingSeeder extends Seeder 'is_featured' => $index % 7 === 0, 'expires_at' => now()->addDays(21 + ($index % 9)), 'created_at' => now()->subHours(6 + $index), - 'image_path' => DemoListingImageFactory::ensure( - $slug, - $title, - $familyName, - $user->name, - $index - ), + 'image_path' => SampleListingImageCatalog::pathFor($category, $index), ]; } @@ -216,7 +209,7 @@ class ListingSeeder extends Seeder $location = trim(collect([$city, $country])->filter()->join(', ')); return sprintf( - '%s listed by %s. Clean demo condition, unique seeded media, and ready for browsing, favorites, inbox, and panel testing. Pickup area: %s.', + '%s listed by %s. Clean demo condition, sample product photo assigned from the provided catalog, and ready for browsing, favorites, inbox, and panel testing. Pickup area: %s.', $categoryName !== '' ? $categoryName : 'Item', trim((string) $user->name) !== '' ? trim((string) $user->name) : 'a marketplace user', $location !== '' ? $location : 'Turkey' @@ -272,8 +265,15 @@ class ListingSeeder extends Seeder return $listing; } - private function syncListingImage(Listing $listing, string $imageAbsolutePath): void + private function syncListingImage(Listing $listing, ?string $imageAbsolutePath): void { - $listing->replacePublicImage($imageAbsolutePath, $listing->slug.'.svg'); + if (! is_string($imageAbsolutePath) || ! is_file($imageAbsolutePath)) { + return; + } + + $listing->replacePublicImage( + $imageAbsolutePath, + SampleListingImageCatalog::fileNameFor($imageAbsolutePath, $listing->slug) + ); } } diff --git a/Modules/Listing/Http/Controllers/ListingController.php b/Modules/Listing/Http/Controllers/ListingController.php index 452b250ab..d50c507fb 100644 --- a/Modules/Listing/Http/Controllers/ListingController.php +++ b/Modules/Listing/Http/Controllers/ListingController.php @@ -178,7 +178,7 @@ class ListingController extends Controller $listing->category_id ? (int) $listing->category_id : null, $listing->custom_fields ?? [], ); - $gallery = $listing->themeGallery(); + $gallery = $listing->galleryImageData(); $listingVideos = $listing->getRelation('videos'); $relatedListings = $listing->relatedSuggestions(12); $themePillCategories = Category::themePills(10); diff --git a/Modules/Listing/Models/Listing.php b/Modules/Listing/Models/Listing.php index ee6dcf4c2..4a1628c49 100644 --- a/Modules/Listing/Models/Listing.php +++ b/Modules/Listing/Models/Listing.php @@ -10,13 +10,16 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Modules\Category\Models\Category; +use Modules\Listing\Support\ListingImageViewData; use Modules\Listing\States\ListingStatus; use Modules\Listing\Support\ListingPanelHelper; use Modules\Video\Models\Video; +use Spatie\Image\Enums\Fit; use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\Traits\LogsActivity; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; +use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\ModelStates\HasStates; use Throwable; @@ -183,22 +186,56 @@ class Listing extends Model implements HasMedia public function themeGallery(): array { - $mediaUrls = $this->getMedia('listing-images') - ->map(fn ($media): string => $media->getUrl()) - ->filter(fn (string $url): bool => $url !== '') + return collect($this->galleryImageData()) + ->map(fn (array $image): ?string => ListingImageViewData::pickUrl($image['gallery'] ?? null)) + ->filter(fn (?string $url): bool => is_string($url) && $url !== '') ->values() ->all(); + } - if ($mediaUrls !== []) { - return $mediaUrls; + public function galleryImageData(): array + { + $mediaItems = $this->getMedia('listing-images'); + + if ($mediaItems->isNotEmpty()) { + return $mediaItems + ->map(fn (Media $media): array => [ + 'gallery' => ListingImageViewData::fromMedia($media, 'gallery'), + 'thumb' => ListingImageViewData::fromMedia($media, 'thumb'), + ]) + ->values() + ->all(); } return collect($this->images ?? []) ->filter(fn ($value): bool => is_string($value) && trim($value) !== '') + ->map(fn (string $url): array => [ + 'gallery' => ListingImageViewData::fromUrl($url), + 'thumb' => ListingImageViewData::fromUrl($url), + ]) ->values() ->all(); } + public function primaryImageData(string $context = 'card'): ?array + { + $media = $this->getFirstMedia('listing-images'); + + if ($media instanceof Media) { + return ListingImageViewData::fromMedia($media, $context); + } + + $fallback = collect($this->images ?? []) + ->first(fn ($value): bool => is_string($value) && trim($value) !== ''); + + return ListingImageViewData::fromUrl(is_string($fallback) ? $fallback : null); + } + + public function primaryImageUrl(string $context = 'card', string $viewport = 'desktop'): ?string + { + return ListingImageViewData::pickUrl($this->primaryImageData($context), $viewport); + } + public function relatedSuggestions(int $limit = 8): Collection { $baseQuery = static::query() @@ -254,9 +291,7 @@ class Listing extends Model implements HasMedia public function panelPrimaryImageUrl(): ?string { - $url = trim((string) $this->getFirstMediaUrl('listing-images')); - - return $url !== '' ? $url : null; + return $this->primaryImageUrl('card', 'desktop'); } public function panelPriceLabel(): string @@ -444,7 +479,73 @@ class Listing extends Model implements HasMedia public function registerMediaCollections(): void { - $this->addMediaCollection('listing-images'); + $this->addMediaCollection('listing-images')->useDisk('public'); + } + + public function registerMediaConversions(?Media $media = null): void + { + if ($this->shouldSkipConversionsForSeeder()) { + return; + } + + $this + ->addMediaConversion('gallery-mobile') + ->fit(Fit::Max, 960, 960) + ->format('webp') + ->quality(78) + ->performOnCollections('listing-images') + ->nonQueued(); + + $this + ->addMediaConversion('gallery-desktop') + ->fit(Fit::Max, 1680, 1680) + ->format('webp') + ->quality(82) + ->performOnCollections('listing-images') + ->nonQueued(); + + $this + ->addMediaConversion('card-mobile') + ->fit(Fit::Crop, 720, 540) + ->format('webp') + ->quality(76) + ->performOnCollections('listing-images') + ->nonQueued(); + + $this + ->addMediaConversion('card-desktop') + ->fit(Fit::Crop, 1080, 810) + ->format('webp') + ->quality(80) + ->performOnCollections('listing-images') + ->nonQueued(); + + $this + ->addMediaConversion('thumb-mobile') + ->fit(Fit::Crop, 220, 220) + ->format('webp') + ->quality(74) + ->performOnCollections('listing-images') + ->nonQueued(); + + $this + ->addMediaConversion('thumb-desktop') + ->fit(Fit::Crop, 320, 320) + ->format('webp') + ->quality(78) + ->performOnCollections('listing-images') + ->nonQueued(); + } + + private function shouldSkipConversionsForSeeder(): bool + { + if (! app()->runningInConsole()) { + return false; + } + + $argv = implode(' ', (array) ($_SERVER['argv'] ?? [])); + + return str_contains($argv, 'db:seed') || str_contains($argv, '--seed'); } protected function location(): Attribute diff --git a/Modules/Listing/Support/DemoListingImageFactory.php b/Modules/Listing/Support/DemoListingImageFactory.php deleted file mode 100644 index c80e3b5f8..000000000 --- a/Modules/Listing/Support/DemoListingImageFactory.php +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - OPENCLASSIFY DEMO - {$shortCategory} - {$shortOwner} - {$shortTitle} - {$code} - -SVG; - - file_put_contents($filePath, $svg); - - return $filePath; - } - - private static function escape(string $value): string - { - return htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8'); - } -} diff --git a/Modules/Listing/Support/ListingImageViewData.php b/Modules/Listing/Support/ListingImageViewData.php new file mode 100644 index 000000000..dbc45aa19 --- /dev/null +++ b/Modules/Listing/Support/ListingImageViewData.php @@ -0,0 +1,63 @@ + ['mobile' => 'card-mobile', 'desktop' => 'card-desktop'], + 'gallery' => ['mobile' => 'gallery-mobile', 'desktop' => 'gallery-desktop'], + 'thumb' => ['mobile' => 'thumb-mobile', 'desktop' => 'thumb-desktop'], + ]; + + public static function fromMedia(Media $media, string $context = 'card'): array + { + $conversion = self::CONTEXTS[$context] ?? self::CONTEXTS['card']; + + return [ + 'mobile' => $media->getAvailableUrl([$conversion['mobile'], $conversion['desktop']]), + 'desktop' => $media->getAvailableUrl([$conversion['desktop'], $conversion['mobile']]), + 'fallback' => $media->getUrl(), + 'alt' => trim((string) $media->name), + ]; + } + + public static function fromUrl(?string $url): ?array + { + $value = is_string($url) ? trim($url) : ''; + + if ($value === '') { + return null; + } + + return [ + 'mobile' => $value, + 'desktop' => $value, + 'fallback' => $value, + 'alt' => '', + ]; + } + + public static function pickUrl(?array $image, string $viewport = 'desktop'): ?string + { + if (! is_array($image)) { + return null; + } + + $preferred = $viewport === 'mobile' + ? ($image['mobile'] ?? null) + : ($image['desktop'] ?? null); + + if (is_string($preferred) && trim($preferred) !== '') { + return trim($preferred); + } + + $fallback = $image['fallback'] ?? null; + + return is_string($fallback) && trim($fallback) !== '' + ? trim($fallback) + : null; + } +} diff --git a/Modules/Listing/Support/SampleListingImageCatalog.php b/Modules/Listing/Support/SampleListingImageCatalog.php new file mode 100644 index 000000000..8d4eaebde --- /dev/null +++ b/Modules/Listing/Support/SampleListingImageCatalog.php @@ -0,0 +1,264 @@ + [ + 'phone.jpeg', + ], + 'electronics-computers' => [ + 'laptop.jpg', + 'macbook.jpg', + 'tech product macbook digital image render macbook pro.jpg', + ], + 'electronics-tablets' => [ + 'tech product macbook digital image render macbook pro.jpg', + 'phone.jpeg', + ], + 'electronics-tvs' => [ + 'headphones.jpg', + 'macbook.jpg', + ], + 'vehicles-cars' => [ + 'car.jpeg', + 'car2.jpeg', + 'sportscars car sports car vehicle.jpg', + ], + 'vehicles-motorcycles' => [ + 'sport motorbike product photography products moto sports bike enduro motorsports clothing.jpg', + ], + 'vehicles-trucks' => [ + 'car2.jpeg', + 'car.jpeg', + ], + 'vehicles-boats' => [ + 'sportscars car sports car vehicle.jpg', + 'car.jpeg', + ], + 'real-estate-for-sale' => [ + 'roof large house fence gate.jpg', + 'house interior design home interior bedroom.jpg', + ], + 'real-estate-for-rent' => [ + 'house interior design home interior bedroom.jpg', + 'roof large house fence gate.jpg', + ], + 'real-estate-commercial' => [ + 'office building black laptop programming grey interior desk men .jpg', + 'roof large house fence gate.jpg', + ], + 'fashion-men' => [ + 'grey product photography hat sustainable fashion beanie ethical fashion ambleside .jpg', + 'sunglasses.jpg', + ], + 'fashion-women' => [ + 'fashion natural wedding product shoes.jpg', + 'sunglasses.jpg', + ], + 'fashion-kids' => [ + 'nike-sport-wear.png', + 'fashion natural wedding product shoes.jpg', + ], + 'fashion-shoes' => [ + 'fashion natural wedding product shoes.jpg', + 'nike-sport-wear.png', + ], + 'home-garden-furniture' => [ + 'house interior design home interior bedroom.jpg', + 'cup.jpg', + ], + 'home-garden-garden' => [ + 'roof large house fence gate.jpg', + 'house interior design home interior bedroom.jpg', + ], + 'home-garden-appliances' => [ + 'cup.jpg', + 'house interior design home interior bedroom.jpg', + ], + 'sports-outdoor' => [ + 'sport motorbike product photography products moto sports bike enduro motorsports clothing.jpg', + 'nike-sport-wear.png', + ], + 'sports-fitness' => [ + 'smart-watch.jpg', + ' watch_band.jpg', + 'nike-sport-wear.png', + ], + 'sports-team-sports' => [ + 'nike-sport-wear.png', + 'smart-watch.jpg', + ], + 'jobs-full-time' => [ + 'business white career hiring recruitment academic jobs.jpg', + 'office business people laptop work team classroom grey teamwork table.jpg', + ], + 'jobs-part-time' => [ + 'jobs.jpg', + 'business white career hiring recruitment academic jobs.jpg', + ], + 'jobs-freelance' => [ + 'office business technology meeting coding grey engineering engineer software engineer professional woman whiteboard tutor.jpg', + 'office building black laptop programming grey interior desk men .jpg', + ], + 'services-cleaning' => [ + 'office business work team white customer service studio office building.jpg', + 'cup.jpg', + ], + 'services-repair' => [ + 'office building black laptop programming grey interior desk men .jpg', + 'office business technology meeting coding grey engineering engineer software engineer professional woman whiteboard tutor.jpg', + ], + 'services-education' => [ + 'office business people laptop work team classroom grey teamwork table.jpg', + 'business white career hiring recruitment academic jobs.jpg', + ], + ]; + + private const FAMILY_IMAGES = [ + 'electronics' => [ + 'phone.jpeg', + 'laptop.jpg', + 'macbook.jpg', + 'tech product macbook digital image render macbook pro.jpg', + 'headphones.jpg', + 'smart-watch.jpg', + ' watch_band.jpg', + ], + 'vehicles' => [ + 'car.jpeg', + 'car2.jpeg', + 'sportscars car sports car vehicle.jpg', + 'sport motorbike product photography products moto sports bike enduro motorsports clothing.jpg', + ], + 'real-estate' => [ + 'roof large house fence gate.jpg', + 'house interior design home interior bedroom.jpg', + 'office building black laptop programming grey interior desk men .jpg', + ], + 'fashion' => [ + 'fashion natural wedding product shoes.jpg', + 'grey product photography hat sustainable fashion beanie ethical fashion ambleside .jpg', + 'nike-sport-wear.png', + 'sunglasses.jpg', + ], + 'home-garden' => [ + 'house interior design home interior bedroom.jpg', + 'roof large house fence gate.jpg', + 'cup.jpg', + ], + 'sports' => [ + 'sport motorbike product photography products moto sports bike enduro motorsports clothing.jpg', + 'nike-sport-wear.png', + 'smart-watch.jpg', + ' watch_band.jpg', + ], + 'jobs' => [ + 'jobs.jpg', + 'business white career hiring recruitment academic jobs.jpg', + 'office business people laptop work team classroom grey teamwork table.jpg', + 'office business technology meeting coding grey engineering engineer software engineer professional woman whiteboard tutor.jpg', + 'vintage red text retro machine sign blur bokeh flag hiring.jpg', + ], + 'services' => [ + 'office business work team white customer service studio office building.jpg', + 'office building black laptop programming grey interior desk men .jpg', + 'office business people laptop work team classroom grey teamwork table.jpg', + 'cup.jpg', + ], + ]; + + public static function pathFor(Category $category, int $seed): ?string + { + $categorySlug = trim((string) $category->slug); + $familySlug = trim((string) ($category->parent?->slug ?? $category->slug)); + + $paths = self::resolvePathsForSlug($categorySlug); + + if ($paths->isEmpty()) { + $paths = self::resolvePathsForSlug($familySlug); + } + + if ($paths->isEmpty()) { + $paths = self::allPaths(); + } + + if ($paths->isEmpty()) { + return null; + } + + return $paths->values()->get($seed % $paths->count()); + } + + public static function fileNameFor(string $absolutePath, string $slug): string + { + $extension = strtolower((string) pathinfo($absolutePath, PATHINFO_EXTENSION)); + + return $slug.($extension !== '' ? '.'.$extension : ''); + } + + private static function resolvePathsForSlug(string $slug): Collection + { + $fileNames = self::CATEGORY_IMAGES[$slug] ?? self::FAMILY_IMAGES[$slug] ?? []; + + return collect($fileNames) + ->map(fn (string $fileName): string => public_path(self::DIRECTORY.'/'.$fileName)) + ->filter(fn (string $path): bool => self::isAllowed($path)) + ->values(); + } + + private static function allPaths(): Collection + { + $paths = glob(public_path(self::DIRECTORY.'/*')) ?: []; + + return collect($paths) + ->filter(function (string $path): bool { + if (! self::isAllowed($path)) { + return false; + } + + $extension = strtolower((string) pathinfo($path, PATHINFO_EXTENSION)); + + return in_array($extension, ['jpg', 'jpeg', 'png', 'webp'], true); + }) + ->values(); + } + + private static function isAllowed(string $path): bool + { + if (! is_file($path)) { + return false; + } + + if (filesize($path) > (int) config('media-library.max_file_size', 10 * 1024 * 1024)) { + return false; + } + + $dimensions = @getimagesize($path); + + if (! is_array($dimensions)) { + return false; + } + + $width = (int) ($dimensions[0] ?? 0); + $height = (int) ($dimensions[1] ?? 0); + + if ($width < 1 || $height < 1) { + return false; + } + + if (max($width, $height) > self::MAX_EDGE) { + return false; + } + + return ($width * $height) <= self::MAX_PIXELS; + } +} diff --git a/Modules/Listing/resources/views/partials/index-content.blade.php b/Modules/Listing/resources/views/partials/index-content.blade.php index df506b651..604c5c9e8 100644 --- a/Modules/Listing/resources/views/partials/index-content.blade.php +++ b/Modules/Listing/resources/views/partials/index-content.blade.php @@ -231,7 +231,7 @@
@foreach($listings as $listing) @php - $listingImage = $listing->getFirstMediaUrl('listing-images'); + $listingImage = $listing->primaryImageData('card'); $isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true); $priceValue = ! is_null($listing->price) ? (float) $listing->price : null; $locationParts = array_filter([ @@ -244,7 +244,11 @@
@if($listingImage) - {{ $listing->title }} + @include('listing::partials.responsive-image', [ + 'image' => $listingImage, + 'alt' => $listing->title, + 'class' => 'w-full h-full object-cover', + ]) @else diff --git a/Modules/Listing/resources/views/partials/responsive-image.blade.php b/Modules/Listing/resources/views/partials/responsive-image.blade.php new file mode 100644 index 000000000..5fd16ca17 --- /dev/null +++ b/Modules/Listing/resources/views/partials/responsive-image.blade.php @@ -0,0 +1,31 @@ +@php + $image = is_array($image ?? null) ? $image : null; + $fallback = is_string($image['fallback'] ?? null) ? trim((string) $image['fallback']) : ''; + $mobile = is_string($image['mobile'] ?? null) ? trim((string) $image['mobile']) : ''; + $desktop = is_string($image['desktop'] ?? null) ? trim((string) $image['desktop']) : ''; + $altText = trim((string) ($alt ?? ($image['alt'] ?? ''))); + $imageClass = trim((string) ($class ?? '')); + $loadingMode = trim((string) ($loading ?? 'lazy')); + $fetchPriority = trim((string) ($fetchpriority ?? '')); + $sizesValue = trim((string) ($sizes ?? '')); +@endphp + +@if($fallback !== '' || $mobile !== '' || $desktop !== '') + + @if($mobile !== '') + + @endif + @if($desktop !== '') + + @endif + {{ $altText }} + +@endif diff --git a/Modules/Listing/resources/views/themes/otoplus/show.blade.php b/Modules/Listing/resources/views/themes/otoplus/show.blade.php index 3e60d68c1..7f15e0103 100644 --- a/Modules/Listing/resources/views/themes/otoplus/show.blade.php +++ b/Modules/Listing/resources/views/themes/otoplus/show.blade.php @@ -22,10 +22,10 @@ $publishedAt = $listing->created_at?->format('M j, Y') ?? 'Recently'; $postedAgo = $listing->created_at?->diffForHumans() ?? 'Listed recently'; $galleryImages = collect($gallery ?? []) - ->filter(fn ($value) => is_string($value) && trim($value) !== '') + ->filter(fn ($value) => is_array($value) && is_array($value['gallery'] ?? null)) ->values() ->all(); - $initialGalleryImage = $galleryImages[0] ?? null; + $initialGalleryImage = $galleryImages[0]['gallery'] ?? null; $galleryCount = count($galleryImages); $description = trim((string) ($listing->description ?? '')); @@ -143,7 +143,23 @@
@if($initialGalleryImage) - {{ $displayTitle }} + + + + {{ $displayTitle }} + @else @endif @@ -171,15 +187,25 @@ @if($galleryImages !== [])
@foreach($galleryImages as $index => $image) + @php + $galleryImage = $image['gallery'] ?? null; + $thumbImage = $image['thumb'] ?? $galleryImage; + @endphp @endforeach
@@ -439,11 +465,7 @@
@foreach(($relatedListings ?? collect()) as $related) @php - $relatedImage = $related->getFirstMediaUrl('listing-images'); - if (! $relatedImage && is_array($related->images ?? null)) { - $relatedImage = collect($related->images)->first(); - } - + $relatedImage = $related->primaryImageData('card'); $relatedPrice = 'Price on request'; if (! is_null($related->price)) { $relatedPriceValue = (float) $related->price; @@ -458,7 +480,11 @@
@if($relatedImage) - {{ $related->title }} + @include('listing::partials.responsive-image', [ + 'image' => $relatedImage, + 'alt' => $related->title, + 'class' => 'w-full h-full object-cover', + ]) @endif
@@ -496,6 +522,8 @@ (() => { document.querySelectorAll('[data-gallery]').forEach((galleryRoot) => { const mainImage = galleryRoot.querySelector('[data-gallery-main]'); + const mainMobileSource = galleryRoot.querySelector('[data-gallery-source-mobile]'); + const mainDesktopSource = galleryRoot.querySelector('[data-gallery-source-desktop]'); const thumbButtons = Array.from(galleryRoot.querySelectorAll('[data-gallery-thumb]')); const prevButton = galleryRoot.querySelector('[data-gallery-prev]'); const nextButton = galleryRoot.querySelector('[data-gallery-next]'); @@ -517,9 +545,20 @@ } activeIndex = index; - const src = thumbButtons[index].dataset.gallerySrc; - if (src) { - mainImage.src = src; + const mobileSrc = thumbButtons[index].dataset.galleryMobileSrc || ''; + const desktopSrc = thumbButtons[index].dataset.galleryDesktopSrc || ''; + const fallbackSrc = thumbButtons[index].dataset.galleryFallbackSrc || desktopSrc || mobileSrc; + + if (mainMobileSource && mobileSrc) { + mainMobileSource.srcset = mobileSrc; + } + + if (mainDesktopSource && desktopSrc) { + mainDesktopSource.srcset = desktopSrc; + } + + if (fallbackSrc) { + mainImage.src = fallbackSrc; } if (currentCounter) { diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 64680df05..fcca05e25 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -3,7 +3,7 @@ @php $menuCategories = $categories->take(8); $heroListing = $featuredListings->first() ?? $recentListings->first(); - $heroImage = $heroListing?->getFirstMediaUrl('listing-images'); + $heroImage = $heroListing?->primaryImageData('gallery'); $listingCards = $recentListings->take(6); $demoEnabled = (bool) config('demo.enabled'); $prepareDemoRoute = $demoEnabled ? route('demo.prepare') : null; @@ -180,7 +180,13 @@ @if($slide['image_url']) {{ $slide['title'] }} @elseif($heroImage) - {{ $heroListing?->title }} + @include('listing::partials.responsive-image', [ + 'image' => $heroImage, + 'alt' => $heroListing?->title, + 'class' => 'w-full h-full object-cover rounded-2xl', + 'loading' => 'eager', + 'fetchpriority' => 'high', + ]) @else
@@ -274,7 +280,7 @@
@forelse($listingCards as $listing) @php - $listingImage = $listing->getFirstMediaUrl('listing-images'); + $listingImage = $listing->primaryImageData('card'); $priceLabel = $listing->price ? number_format((float) $listing->price, 0).' '.$listing->currency : __('messages.free'); $locationLabel = trim(collect([$listing->city, $listing->country])->filter()->join(', ')); $isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true); @@ -283,7 +289,11 @@
@if($listingImage) - {{ $listing->title }} + @include('listing::partials.responsive-image', [ + 'image' => $listingImage, + 'alt' => $listing->title, + 'class' => 'w-full h-full object-cover', + ]) @else
diff --git a/resources/views/panel/listings.blade.php b/resources/views/panel/listings.blade.php index b4c5de948..07a83045b 100644 --- a/resources/views/panel/listings.blade.php +++ b/resources/views/panel/listings.blade.php @@ -131,7 +131,7 @@ @forelse ($listings as $listing) @php $statusMeta = $listing->panelStatusMeta(); - $listingImage = $listing->panelPrimaryImageUrl(); + $listingImage = $listing->primaryImageData('card'); $priceLabel = $listing->panelPriceLabel(); $favoriteCount = (int) ($listing->favorited_by_users_count ?? 0); $viewCount = (int) ($listing->view_count ?? 0); @@ -147,7 +147,11 @@ {{ $listing->title }} + @include('listing::partials.responsive-image', [ + 'image' => $listingImage, + 'alt' => $listing->title, + 'class' => 'h-full w-full object-cover', + ]) @else
No image diff --git a/storage/media-library/temp/6enz4BMgOndKhXqhvCYEbpN04F4wK7i5/demo-a-at-acom-sports-fitness-gallery-mobile.webp b/storage/media-library/temp/6enz4BMgOndKhXqhvCYEbpN04F4wK7i5/demo-a-at-acom-sports-fitness-gallery-mobile.webp new file mode 100644 index 000000000..fde0875ff Binary files /dev/null and b/storage/media-library/temp/6enz4BMgOndKhXqhvCYEbpN04F4wK7i5/demo-a-at-acom-sports-fitness-gallery-mobile.webp differ diff --git a/storage/media-library/temp/6enz4BMgOndKhXqhvCYEbpN04F4wK7i5/nAXzStsh1302gdWEg7oiTqUWP3f8DjxN.jpg b/storage/media-library/temp/6enz4BMgOndKhXqhvCYEbpN04F4wK7i5/nAXzStsh1302gdWEg7oiTqUWP3f8DjxN.jpg new file mode 100644 index 000000000..87a2d5154 Binary files /dev/null and b/storage/media-library/temp/6enz4BMgOndKhXqhvCYEbpN04F4wK7i5/nAXzStsh1302gdWEg7oiTqUWP3f8DjxN.jpg differ diff --git a/storage/media-library/temp/6enz4BMgOndKhXqhvCYEbpN04F4wK7i5/rkiOPLaVlMWgFIo6xjL5PfK8e1q0AB2ugallery-desktop.jpg b/storage/media-library/temp/6enz4BMgOndKhXqhvCYEbpN04F4wK7i5/rkiOPLaVlMWgFIo6xjL5PfK8e1q0AB2ugallery-desktop.jpg new file mode 100644 index 000000000..e69de29bb diff --git a/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-card-desktop.webp b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-card-desktop.webp new file mode 100644 index 000000000..01382f819 Binary files /dev/null and b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-card-desktop.webp differ diff --git a/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-card-mobile.webp b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-card-mobile.webp new file mode 100644 index 000000000..ebf7fac6e Binary files /dev/null and b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-card-mobile.webp differ diff --git a/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-gallery-desktop.webp b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-gallery-desktop.webp new file mode 100644 index 000000000..45c446c6f Binary files /dev/null and b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-gallery-desktop.webp differ diff --git a/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-gallery-mobile.webp b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-gallery-mobile.webp new file mode 100644 index 000000000..590416b5a Binary files /dev/null and b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-gallery-mobile.webp differ diff --git a/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-thumb-mobile.webp b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-thumb-mobile.webp new file mode 100644 index 000000000..652b0a4da Binary files /dev/null and b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/demo-d-at-dcom-jobs-freelance-thumb-mobile.webp differ diff --git a/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/uGMJT8Lc24jVROCYL4c6LaIew1UpM7gk.jpeg b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/uGMJT8Lc24jVROCYL4c6LaIew1UpM7gk.jpeg new file mode 100644 index 000000000..c897de083 Binary files /dev/null and b/storage/media-library/temp/CpzBugtAPB6fgeiSBRf4AuVlS1OP7GcM/uGMJT8Lc24jVROCYL4c6LaIew1UpM7gk.jpeg differ diff --git a/storage/media-library/temp/P7uRGZ157eenxjZUdKjMLkawDhEbcDth/X6DQTZsZQUUhQwNkt0otVWmTLVV7nLyn.jpg b/storage/media-library/temp/P7uRGZ157eenxjZUdKjMLkawDhEbcDth/X6DQTZsZQUUhQwNkt0otVWmTLVV7nLyn.jpg new file mode 100644 index 000000000..e90a2352a Binary files /dev/null and b/storage/media-library/temp/P7uRGZ157eenxjZUdKjMLkawDhEbcDth/X6DQTZsZQUUhQwNkt0otVWmTLVV7nLyn.jpg differ diff --git a/storage/media-library/temp/P7uRGZ157eenxjZUdKjMLkawDhEbcDth/v7jZLmJgqKUNzsDv1Nvzc7NEiAW9jsOjgallery-mobile.jpg b/storage/media-library/temp/P7uRGZ157eenxjZUdKjMLkawDhEbcDth/v7jZLmJgqKUNzsDv1Nvzc7NEiAW9jsOjgallery-mobile.jpg new file mode 100644 index 000000000..e90a2352a Binary files /dev/null and b/storage/media-library/temp/P7uRGZ157eenxjZUdKjMLkawDhEbcDth/v7jZLmJgqKUNzsDv1Nvzc7NEiAW9jsOjgallery-mobile.jpg differ