Fix duplicate listing images

This commit is contained in:
fatihalp 2026-03-10 04:35:25 +03:00
parent f8c953d37c
commit 6ea371e372
3 changed files with 472 additions and 38 deletions

View File

@ -180,6 +180,12 @@ final class SampleListingImageCatalog
{
return self::allPaths()
->sortBy(fn (string $path): string => strtolower((string) basename($path)))
->map(fn (string $path): array => [
'path' => $path,
'hash' => md5_file($path) ?: strtolower((string) basename($path)),
])
->unique('hash')
->pluck('path')
->values();
}
@ -201,8 +207,12 @@ final class SampleListingImageCatalog
public static function fileNameFor(string $absolutePath, string $slug): string
{
$extension = strtolower((string) pathinfo($absolutePath, PATHINFO_EXTENSION));
$hash = md5_file($absolutePath);
$hashSuffix = is_string($hash) && $hash !== ''
? '-'.substr($hash, 0, 8)
: '';
return $slug.($extension !== '' ? '.'.$extension : '');
return $slug.$hashSuffix.($extension !== '' ? '.'.$extension : '');
}
private static function resolvePathsForSlug(string $slug): Collection

View File

@ -21,13 +21,28 @@
'search' => $search !== '' ? $search : null,
'user' => $sellerUserId ?? null,
], $normalizeQuery);
$activeFilterCount = collect([
$categoryId,
$countryId,
$cityId,
$sellerUserId,
$minPriceInput !== '' ? $minPriceInput : null,
$maxPriceInput !== '' ? $maxPriceInput : null,
$dateFilter !== 'all' ? $dateFilter : null,
])->filter($normalizeQuery)->count();
@endphp
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
<div class="listing-index-shell max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
<h1 class="sr-only">{{ $seoHeading }}</h1>
<div class="grid grid-cols-1 lg:grid-cols-[260px,1fr] gap-4 lg:gap-5">
<aside class="space-y-4">
<aside class="listing-sidebar" data-listing-filter-drawer aria-hidden="false">
<button type="button" class="listing-sidebar-backdrop lg:hidden" data-listing-filter-close aria-label="Close filters"></button>
<div class="listing-sidebar-shell space-y-4">
<div class="listing-sidebar-head lg:hidden">
<h2>Filters</h2>
<button type="button" class="listing-sidebar-close" data-listing-filter-close aria-label="Close filters">×</button>
</div>
<section class="listing-filter-card p-4">
<div class="flex items-center justify-between gap-3 mb-3">
<h2 class="text-2xl font-bold text-slate-900 leading-none">Categories</h2>
@ -159,10 +174,62 @@
</button>
</div>
</form>
</div>
</aside>
<section class="space-y-4">
<div class="listing-filter-card px-4 py-3 flex flex-col xl:flex-row xl:items-center gap-3">
<div class="listing-mobile-toolbar lg:hidden">
<div class="listing-mobile-toolbar-row">
<button type="button" class="listing-mobile-filter-button" data-listing-filter-open>
Filters
@if($activeFilterCount > 0)
<span class="listing-mobile-filter-badge">{{ $activeFilterCount }}</span>
@endif
</button>
<form method="GET" action="{{ route('listings.index') }}" class="listing-mobile-sort-form">
@if($search !== '')
<input type="hidden" name="search" value="{{ $search }}">
@endif
@if($categoryId)
<input type="hidden" name="category" value="{{ $categoryId }}">
@endif
@if(! empty($sellerUserId))
<input type="hidden" name="user" value="{{ $sellerUserId }}">
@endif
@if($countryId)
<input type="hidden" name="country" value="{{ $countryId }}">
@endif
@if($cityId)
<input type="hidden" name="city" value="{{ $cityId }}">
@endif
@if($minPriceInput !== '')
<input type="hidden" name="min_price" value="{{ $minPriceInput }}">
@endif
@if($maxPriceInput !== '')
<input type="hidden" name="max_price" value="{{ $maxPriceInput }}">
@endif
@if($dateFilter !== 'all')
<input type="hidden" name="date_filter" value="{{ $dateFilter }}">
@endif
<label class="listing-mobile-sort-label">
<span>Sort</span>
<select name="sort" class="listing-mobile-sort-select" onchange="this.form.submit()">
<option value="smart" @selected($sort === 'smart')>Recommended</option>
<option value="newest" @selected($sort === 'newest')>Newest</option>
<option value="oldest" @selected($sort === 'oldest')>Oldest</option>
<option value="price_asc" @selected($sort === 'price_asc')>Price </option>
<option value="price_desc" @selected($sort === 'price_desc')>Price </option>
</select>
</label>
</form>
</div>
<p class="listing-mobile-toolbar-meta">
<strong>{{ number_format($resultListingsCount) }}</strong>
{{ $activeCategoryName !== '' ? ' listings in '.$activeCategoryName : ' listings found' }}
</p>
</div>
<div class="listing-filter-card px-4 py-3 hidden lg:flex flex-col xl:flex-row xl:items-center gap-3">
<p class="text-sm text-slate-700 mr-auto">
<strong>{{ number_format($resultListingsCount) }}</strong>
{{ $activeCategoryName !== '' ? ' listings found in '.$activeCategoryName : ' listings found' }}
@ -228,7 +295,7 @@
No listings match this filter.
</div>
@else
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3.5">
<div class="grid grid-cols-2 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3">
@foreach($listings as $listing)
@php
$listingImage = $listing->primaryImageData('card');
@ -241,7 +308,7 @@
$locationText = implode(', ', $locationParts);
@endphp
<article class="listing-card">
<div class="relative h-52 bg-slate-200">
<div class="relative h-40 sm:h-48 lg:h-52 bg-slate-200">
@if($listingImage)
<a href="{{ route('listings.show', $listing) }}" class="block w-full h-full">
@include('listing::partials.responsive-image', [
@ -282,7 +349,7 @@
<div class="px-3.5 py-3">
<a href="{{ route('listings.show', $listing) }}" class="block">
<p class="text-3xl leading-none font-bold text-slate-900">
<p class="text-xl sm:text-2xl lg:text-3xl leading-none font-bold text-slate-900">
@if(!is_null($priceValue) && $priceValue > 0)
{{ number_format($priceValue, 0) }} {{ $listing->currency }}
@else
@ -320,8 +387,60 @@
const countrySelect = document.querySelector('[data-listing-country]');
const citySelect = document.querySelector('[data-listing-city]');
const currentLocationButton = document.querySelector('[data-use-current-location]');
const filterDrawer = document.querySelector('[data-listing-filter-drawer]');
const filterOpenButtons = Array.from(document.querySelectorAll('[data-listing-filter-open]'));
const filterCloseButtons = Array.from(document.querySelectorAll('[data-listing-filter-close]'));
const citiesTemplate = countrySelect?.dataset.citiesUrlTemplate ?? '';
const locationStorageKey = 'oc2.header.location';
const drawerMediaQuery = window.matchMedia('(max-width: 1023px)');
const setDrawerExpanded = (expanded) => {
filterOpenButtons.forEach((button) => button.setAttribute('aria-expanded', expanded ? 'true' : 'false'));
};
const closeFilterDrawer = () => {
if (!filterDrawer) {
return;
}
filterDrawer.classList.remove('is-open');
filterDrawer.setAttribute('aria-hidden', 'true');
document.body.classList.remove('listing-filters-open');
setDrawerExpanded(false);
};
const openFilterDrawer = () => {
if (!filterDrawer || !drawerMediaQuery.matches) {
return;
}
filterDrawer.classList.add('is-open');
filterDrawer.setAttribute('aria-hidden', 'false');
document.body.classList.add('listing-filters-open');
setDrawerExpanded(true);
};
filterOpenButtons.forEach((button) => button.addEventListener('click', openFilterDrawer));
filterCloseButtons.forEach((button) => button.addEventListener('click', closeFilterDrawer));
window.addEventListener('resize', () => {
if (!drawerMediaQuery.matches) {
closeFilterDrawer();
}
});
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeFilterDrawer();
}
});
if (drawerMediaQuery.matches) {
closeFilterDrawer();
} else if (filterDrawer) {
filterDrawer.setAttribute('aria-hidden', 'false');
setDrawerExpanded(false);
}
if (!countrySelect || !citySelect || citiesTemplate === '') {
return;

View File

@ -4138,3 +4138,308 @@ textarea {
align-items: flex-start;
}
}
.listing-index-shell .listing-filter-card,
.listing-index-shell .listing-card {
text-align: left;
}
.listing-mobile-toolbar {
display: none;
}
.listing-mobile-toolbar-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.listing-mobile-filter-button {
min-height: 42px;
border: 1px solid rgba(29, 29, 31, 0.12);
border-radius: 999px;
background: #fff;
padding: 0 14px;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.86rem;
font-weight: 700;
color: #1f2937;
}
.listing-mobile-filter-badge {
min-width: 1.2rem;
height: 1.2rem;
border-radius: 999px;
background: #ff375f;
color: #fff;
font-size: 0.72rem;
font-weight: 800;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
}
.listing-mobile-sort-form {
margin: 0;
}
.listing-mobile-sort-label {
min-height: 42px;
border: 1px solid rgba(29, 29, 31, 0.12);
border-radius: 999px;
background: #fff;
padding: 0 12px;
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.82rem;
font-weight: 700;
color: #4b5563;
flex: 1 1 auto;
justify-content: space-between;
min-width: 0;
}
.listing-mobile-sort-select {
border: 0;
background: transparent;
color: #111827;
font-size: 0.82rem;
font-weight: 700;
outline: none;
max-width: 8.5rem;
}
.listing-mobile-toolbar-meta {
margin: 0;
font-size: 0.82rem;
color: #4b5563;
}
.listing-sidebar {
position: relative;
}
.listing-sidebar-head,
.listing-sidebar-backdrop {
display: none;
}
.listing-sidebar-close {
width: 2.25rem;
height: 2.25rem;
border: 0;
border-radius: 999px;
background: #eef2f7;
color: #475569;
font-size: 1.3rem;
line-height: 1;
}
body.listing-filters-open {
overflow: hidden;
}
@media (max-width: 1023px) {
.listing-mobile-toolbar {
display: grid;
gap: 8px;
border: 1px solid #d9e2ef;
border-radius: 16px;
background: #fff;
padding: 12px;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
}
.listing-sidebar {
position: fixed;
inset: 0;
z-index: 130;
display: none;
padding: 14px;
}
.listing-sidebar.is-open {
display: block;
}
.listing-sidebar-backdrop {
display: block;
position: absolute;
inset: 0;
border: 0;
background: rgba(15, 23, 42, 0.34);
backdrop-filter: saturate(130%) blur(5px);
}
.listing-sidebar-shell {
position: relative;
z-index: 1;
height: 100%;
overflow: auto;
background: #f8fafc;
border: 1px solid #d9e2ef;
border-radius: 24px;
padding: 14px;
}
.listing-sidebar-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.listing-sidebar-head h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 800;
color: #0f172a;
}
.listing-sidebar .listing-filter-card {
box-shadow: none;
border-radius: 16px;
}
}
@media (min-width: 1024px) {
body.listing-filters-open {
overflow: auto;
}
.listing-sidebar {
position: static;
display: block;
inset: auto;
padding: 0;
}
.listing-sidebar-shell {
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
height: auto;
overflow: visible;
}
}
@media (max-width: 639px) {
.oc-nav-wrap {
padding: 8px 10px 10px;
}
.oc-nav-main {
gap: 8px 8px;
}
.oc-topbar {
gap: 8px;
}
.brand-text {
max-width: 6rem;
font-size: 1rem;
}
.oc-actions {
gap: 6px;
min-width: 0;
}
.oc-location {
flex: 0 0 auto;
}
.oc-location-trigger {
min-height: 40px;
min-width: 44px;
padding: 0 10px;
gap: 6px;
justify-content: center;
}
.oc-location-label {
display: none;
}
.oc-location-trigger svg:last-child {
display: none;
}
.oc-account-trigger {
min-height: 40px;
padding: 0 10px;
gap: 6px;
}
.oc-account-name {
max-width: 4.6rem;
font-size: 0.82rem;
}
.oc-cta {
min-height: 40px;
padding: 0 13px;
font-size: 0.86rem;
}
.oc-search {
min-height: 46px;
padding: 0 13px;
gap: 8px;
}
}
@media (max-width: 420px) {
.oc-nav-main {
gap: 6px 6px;
}
.oc-actions {
gap: 4px;
}
.header-utility {
width: 2.35rem;
height: 2.35rem;
flex-basis: 2.35rem;
}
.oc-location-trigger {
min-width: 40px;
padding: 0 8px;
}
.oc-account-trigger {
padding: 0 8px;
}
.oc-account-chevron {
display: none;
}
.oc-account-name {
max-width: 3.5rem;
}
}
@media (max-width: 380px) {
.brand-text {
max-width: 5.25rem;
font-size: 0.94rem;
}
.oc-account-name {
max-width: 3.8rem;
}
.oc-cta {
padding: 0 11px;
}
}