diff --git a/Modules/Category/resources/views/index.blade.php b/Modules/Category/resources/views/index.blade.php index ad08cf022..b9ec19552 100644 --- a/Modules/Category/resources/views/index.blade.php +++ b/Modules/Category/resources/views/index.blade.php @@ -1,15 +1,5 @@ @extends('app::layouts.app') + @section('content') -
-

{{ __('messages.categories') }}

-
- @foreach($categories as $category) - -
{{ $category->icon ?? '📦' }}
-

{{ $category->name }}

-

{{ $category->children->count() }} subcategories

-
- @endforeach -
-
+ @include('category::partials.index-content', ['categories' => $categories]) @endsection diff --git a/Modules/Category/resources/views/partials/index-content.blade.php b/Modules/Category/resources/views/partials/index-content.blade.php new file mode 100644 index 000000000..1a1c5f267 --- /dev/null +++ b/Modules/Category/resources/views/partials/index-content.blade.php @@ -0,0 +1,118 @@ +@php + $categoryCount = $categories->count(); + $subcategoryCount = $categories->sum(fn ($category) => $category->children->count()); +@endphp + +
+
+
+
+ + Browse categories + +
+

+ Find the right marketplace section without leaving the same frontend shell. +

+

+ Explore every top-level category from one clean directory. Header, footer, spacing, and navigation now stay aligned with the rest of the site. +

+
+ +
+ +
+
+

Root categories

+

{{ number_format($categoryCount, 0, '.', ',') }}

+

Only top-level sections are shown first for a simpler directory.

+
+
+

Subcategories

+

{{ number_format($subcategoryCount, 0, '.', ',') }}

+

Each card previews its most relevant child sections before you drill in.

+
+
+
+
+ +
+
+
+

All categories

+

A single directory view with the same spacing and chrome used across the frontend.

+
+

{{ number_format($categoryCount, 0, '.', ',') }} categories

+
+ +
+ @foreach($categories as $category) + @php + $childNames = $category->children + ->take(3) + ->pluck('name') + ->filter() + ->implode(' · '); + $extraChildCount = max($category->children->count() - 3, 0); + $icon = match (trim((string) ($category->icon ?? ''))) { + 'laptop' => 'heroicon-o-computer-desktop', + 'car' => 'heroicon-o-truck', + 'home' => 'heroicon-o-home', + 'shirt' => 'heroicon-o-shopping-bag', + 'sofa' => 'heroicon-o-home-modern', + 'football' => 'heroicon-o-trophy', + 'briefcase' => 'heroicon-o-briefcase', + 'wrench' => 'heroicon-o-wrench-screwdriver', + default => null, + }; + $iconLabel = strtoupper(\Illuminate\Support\Str::substr($category->name, 0, 1)); + @endphp + +
+ + @if($icon) + + @else + {{ $iconLabel }} + @endif + + + {{ number_format($category->children->count(), 0, '.', ',') }} subcategories + +
+ +
+

+ {{ $category->name }} +

+

+ {{ $childNames !== '' ? $childNames : 'Open this category to browse available listings and subcategories.' }} + @if($extraChildCount > 0) + +{{ $extraChildCount }} more + @endif +

+
+ +
+ + Explore category + + +
+
+ @endforeach +
+
+
diff --git a/Modules/Category/resources/views/themes/default/index.blade.php b/Modules/Category/resources/views/themes/default/index.blade.php index ad08cf022..b9ec19552 100644 --- a/Modules/Category/resources/views/themes/default/index.blade.php +++ b/Modules/Category/resources/views/themes/default/index.blade.php @@ -1,15 +1,5 @@ @extends('app::layouts.app') + @section('content') -
-

{{ __('messages.categories') }}

-
- @foreach($categories as $category) - -
{{ $category->icon ?? '📦' }}
-

{{ $category->name }}

-

{{ $category->children->count() }} subcategories

-
- @endforeach -
-
+ @include('category::partials.index-content', ['categories' => $categories]) @endsection diff --git a/Modules/Category/resources/views/themes/otoplus/index.blade.php b/Modules/Category/resources/views/themes/otoplus/index.blade.php index ad08cf022..b9ec19552 100644 --- a/Modules/Category/resources/views/themes/otoplus/index.blade.php +++ b/Modules/Category/resources/views/themes/otoplus/index.blade.php @@ -1,15 +1,5 @@ @extends('app::layouts.app') + @section('content') -
-

{{ __('messages.categories') }}

-
- @foreach($categories as $category) - -
{{ $category->icon ?? '📦' }}
-

{{ $category->name }}

-

{{ $category->children->count() }} subcategories

-
- @endforeach -
-
+ @include('category::partials.index-content', ['categories' => $categories]) @endsection diff --git a/Modules/Listing/Models/Listing.php b/Modules/Listing/Models/Listing.php index 192d84010..15a079f20 100644 --- a/Modules/Listing/Models/Listing.php +++ b/Modules/Listing/Models/Listing.php @@ -23,6 +23,8 @@ class Listing extends Model implements HasMedia { use HasFactory, HasStates, InteractsWithMedia, LogsActivity; + private const DEFAULT_PANEL_EXPIRY_WINDOW_DAYS = 30; + protected $fillable = [ 'title', 'description', 'price', 'currency', 'category_id', 'user_id', 'status', 'images', 'custom_fields', 'slug', @@ -240,11 +242,109 @@ class Listing extends Model implements HasMedia return [ 'all' => (int) $counts->sum(), + 'active' => (int) ($counts['active'] ?? 0), + 'pending' => (int) ($counts['pending'] ?? 0), 'sold' => (int) ($counts['sold'] ?? 0), 'expired' => (int) ($counts['expired'] ?? 0), ]; } + public function panelPrimaryImageUrl(): ?string + { + $url = trim((string) $this->getFirstMediaUrl('listing-images')); + + return $url !== '' ? $url : null; + } + + public function panelPriceLabel(): string + { + if (is_null($this->price)) { + return 'Ücretsiz'; + } + + return number_format((float) $this->price, 2, ',', '.').' '.($this->currency ?? 'TL'); + } + + public function panelStatusMeta(): array + { + return match ($this->statusValue()) { + 'sold' => [ + 'label' => 'Satıldı', + 'badge_class' => 'is-success', + 'hint' => 'İlan satıldı olarak işaretlendi.', + ], + 'expired' => [ + 'label' => 'Süresi doldu', + 'badge_class' => 'is-danger', + 'hint' => 'Yeniden yayına alınmayı bekliyor.', + ], + 'pending' => [ + 'label' => 'İncelemede', + 'badge_class' => 'is-warning', + 'hint' => 'Moderasyon onayı bekleniyor.', + ], + default => [ + 'label' => 'Yayında', + 'badge_class' => 'is-primary', + 'hint' => 'Şu anda ziyaretçilere görünüyor.', + ], + }; + } + + public function panelLocationLabel(): string + { + $parts = collect([ + trim((string) $this->city), + trim((string) $this->country), + ])->filter()->values(); + + return $parts->isNotEmpty() ? $parts->implode(', ') : 'Konum belirtilmedi'; + } + + public function panelPublishedAt(): ?Carbon + { + $createdAt = $this->created_at?->copy(); + $expiresAt = $this->expires_at?->copy(); + + if (! $createdAt) { + return $expiresAt; + } + + if (! $expiresAt || $expiresAt->greaterThanOrEqualTo($createdAt)) { + return $createdAt; + } + + return $expiresAt->subDays(self::DEFAULT_PANEL_EXPIRY_WINDOW_DAYS); + } + + public function panelExpirySummary(): string + { + if (! $this->expires_at) { + return 'Süre sınırı yok'; + } + + $expiresAt = $this->expires_at->copy()->startOfDay(); + $days = Carbon::today()->diffInDays($expiresAt, false); + + return match (true) { + $days > 0 => $days.' gün kaldı', + $days === 0 => 'Bugün sona eriyor', + default => abs($days).' gün önce sona erdi', + }; + } + + public function panelVideoSummary(int $total, int $ready, int $pending): ?array + { + if ($total < 1) { + return null; + } + + return [ + 'label' => $total.' video', + 'detail' => $ready.' hazır'.($pending > 0 ? ', '.$pending.' işleniyor' : ''), + ]; + } + public function statusValue(): string { return $this->status instanceof ListingStatus diff --git a/Modules/Listing/resources/views/themes/otoplus/show.blade.php b/Modules/Listing/resources/views/themes/otoplus/show.blade.php index 5ddb80e4f..86827157b 100644 --- a/Modules/Listing/resources/views/themes/otoplus/show.blade.php +++ b/Modules/Listing/resources/views/themes/otoplus/show.blade.php @@ -17,7 +17,7 @@ $locationLabel = collect([$listing->city, $listing->country]) ->filter(fn ($value) => is_string($value) && trim($value) !== '') - ->implode(', '); + ->implode(' / '); $publishedAt = $listing->created_at?->format('M j, Y') ?? 'Recently'; $postedAgo = $listing->created_at?->diffForHumans() ?? 'Listed recently'; @@ -37,6 +37,7 @@ ? 'Member since '.$listing->user->created_at->format('M Y') : 'New seller'; + $referenceCode = '#'.str_pad((string) $listing->getKey(), 8, '0', STR_PAD_LEFT); $canContactSeller = $listing->user && (! auth()->check() || (int) auth()->id() !== (int) $listing->user_id); $isOwnListing = auth()->check() && (int) auth()->id() === (int) $listing->user_id; @@ -52,35 +53,31 @@ $mapQuery = filled($listing->latitude) && filled($listing->longitude) ? trim((string) $listing->latitude).','.trim((string) $listing->longitude) - : $locationLabel; + : str_replace(' / ', ', ', $locationLabel); $mapUrl = $mapQuery !== '' ? 'https://www.google.com/maps/search/?api=1&query='.urlencode($mapQuery) : null; $reportEmail = config('mail.from.address', 'support@example.com'); - $reportUrl = 'mailto:'.$reportEmail.'?subject='.rawurlencode('Report listing #'.$listing->getKey()); + $reportUrl = 'mailto:'.$reportEmail.'?subject='.rawurlencode('Report listing '.$referenceCode); $shareUrl = route('listings.show', $listing); - $overviewItems = collect([ - ['label' => 'Listing ID', 'value' => '#'.$listing->getKey()], - ['label' => 'Category', 'value' => $listing->category?->name ?? 'General'], - ['label' => 'Location', 'value' => $locationLabel !== '' ? $locationLabel : 'Not specified'], + $specRows = collect([ + ['label' => 'Price', 'value' => $priceLabel], ['label' => 'Published', 'value' => $publishedAt], + ['label' => 'Listing ID', 'value' => $referenceCode], + ['label' => 'Category', 'value' => $listing->category?->name ?? 'General'], + ['label' => 'Location', 'value' => $locationLabel !== '' ? str_replace(' / ', ' / ', $locationLabel) : 'Not specified'], ]) - ->filter(fn (array $item) => trim((string) $item['value']) !== '') + ->merge( + collect($presentableCustomFields ?? [])->map(fn (array $field) => [ + 'label' => trim((string) ($field['label'] ?? '')), + 'value' => trim((string) ($field['value'] ?? '')), + ]) + ) + ->filter(fn (array $item) => $item['label'] !== '' && $item['value'] !== '') + ->unique(fn (array $item) => mb_strtolower($item['label'])) ->values(); - - $detailItems = collect($presentableCustomFields ?? []) - ->map(fn (array $field) => [ - 'label' => trim((string) ($field['label'] ?? '')), - 'value' => trim((string) ($field['value'] ?? '')), - ]) - ->filter(fn (array $field) => $field['label'] !== '' && $field['value'] !== '') - ->values(); - - if ($detailItems->isEmpty()) { - $detailItems = $overviewItems; - } @endphp
@@ -94,155 +91,227 @@ {{ $displayTitle }} +
+
+
+

{{ $listing->category?->name ?? 'Marketplace listing' }}

+

{{ $displayTitle }}

+
+ {{ $referenceCode }} + {{ $sellerName }} + {{ $postedAgo }} +
+
+ +
+
{{ $priceLabel }}
+ @if($locationLabel !== '') +
{{ $locationLabel }}
+ @endif + +
+ + + @auth +
+ @csrf + +
+ @else + Save listing + @endauth +
+
+
+
+
-
- @@ -418,8 +490,10 @@