mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
Fix listings language and actions
This commit is contained in:
parent
08aad25594
commit
f1e2199fef
@ -97,29 +97,29 @@ class ManageGeneralSettings extends SettingsPage
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('site_name')
|
||||
->label('Site Adı')
|
||||
->label('Site Name')
|
||||
->default($defaults['site_name'])
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Textarea::make('site_description')
|
||||
->label('Site Açıklaması')
|
||||
->label('Site Description')
|
||||
->default($defaults['site_description'])
|
||||
->rows(3)
|
||||
->maxLength(500),
|
||||
Select::make('media_disk')
|
||||
->label('Medya Depolama')
|
||||
->label('Media Storage')
|
||||
->options(MediaStorage::options())
|
||||
->default($defaults['media_disk'])
|
||||
->required()
|
||||
->native(false)
|
||||
->helperText('İlan resimleri, videolar, logo ve slide görselleri için kullanılacak depolama sürücüsü.'),
|
||||
->helperText('Storage driver used for listing images, videos, the site logo, and home slide visuals.'),
|
||||
HomeSlideFormSchema::make(
|
||||
$defaults['home_slides'],
|
||||
fn ($state): array => $this->normalizeHomeSlides($state, MediaStorage::activeDisk()),
|
||||
),
|
||||
Hidden::make('site_logo_disk'),
|
||||
FileUpload::make('site_logo')
|
||||
->label('Site Logosu')
|
||||
->label('Site Logo')
|
||||
->image()
|
||||
->disk(fn (Get $get): string => MediaStorage::storedDisk($get('site_logo_disk'), $get('media_disk')))
|
||||
->directory('settings')
|
||||
@ -133,32 +133,32 @@ class ManageGeneralSettings extends SettingsPage
|
||||
);
|
||||
}),
|
||||
TextInput::make('sender_name')
|
||||
->label('Gönderici Adı')
|
||||
->label('Sender Name')
|
||||
->default($defaults['sender_name'])
|
||||
->required()
|
||||
->maxLength(120),
|
||||
TextInput::make('sender_email')
|
||||
->label('Gönderici E-postası')
|
||||
->label('Sender Email')
|
||||
->email()
|
||||
->default($defaults['sender_email'])
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Select::make('default_language')
|
||||
->label('Varsayılan Dil')
|
||||
->label('Default Language')
|
||||
->options($this->localeOptions())
|
||||
->default($defaults['default_language'])
|
||||
->required()
|
||||
->searchable(),
|
||||
CountryCodeSelect::make('default_country_code')
|
||||
->label('Varsayılan Ülke')
|
||||
->label('Default Country')
|
||||
->default($defaults['default_country_code'])
|
||||
->required()
|
||||
->helperText('Panel formlarında varsayılan ülke olarak kullanılır.'),
|
||||
->helperText('Used as the default country in panel forms.'),
|
||||
TagsInput::make('currencies')
|
||||
->label('Para Birimleri')
|
||||
->label('Currencies')
|
||||
->placeholder('TRY')
|
||||
->default($defaults['currencies'])
|
||||
->helperText('TRY, USD, EUR gibi 3 harfli para birimi kodları ekleyin.')
|
||||
->helperText('Add 3-letter currency codes such as TRY, USD, or EUR.')
|
||||
->required()
|
||||
->rules(['array', 'min:1'])
|
||||
->afterStateHydrated(fn (TagsInput $component, $state) => $component->state($this->normalizeCurrencies($state)))
|
||||
@ -181,19 +181,19 @@ class ManageGeneralSettings extends SettingsPage
|
||||
->default($defaults['whatsapp'])
|
||||
->nullable()
|
||||
->formatAsYouType()
|
||||
->helperText('Uluslararası format kullanın. Örnek: +905551112233'),
|
||||
->helperText('Use international format. Example: +905551112233'),
|
||||
Toggle::make('enable_google_maps')
|
||||
->label('Google Maps Aktif')
|
||||
->label('Google Maps Enabled')
|
||||
->default($defaults['enable_google_maps']),
|
||||
TextInput::make('google_maps_api_key')
|
||||
->label('Google Maps API Anahtarı')
|
||||
->label('Google Maps API Key')
|
||||
->password()
|
||||
->revealable()
|
||||
->nullable()
|
||||
->maxLength(255)
|
||||
->helperText('İlan formlarındaki harita alanlarını açmak için gereklidir.'),
|
||||
->helperText('Required to enable map fields in listing forms.'),
|
||||
Toggle::make('enable_google_login')
|
||||
->label('Google ile Giriş Aktif')
|
||||
->label('Google Login Enabled')
|
||||
->default($defaults['enable_google_login']),
|
||||
TextInput::make('google_client_id')
|
||||
->label('Google Client ID')
|
||||
@ -206,7 +206,7 @@ class ManageGeneralSettings extends SettingsPage
|
||||
->nullable()
|
||||
->maxLength(255),
|
||||
Toggle::make('enable_facebook_login')
|
||||
->label('Facebook ile Giriş Aktif')
|
||||
->label('Facebook Login Enabled')
|
||||
->default($defaults['enable_facebook_login']),
|
||||
TextInput::make('facebook_client_id')
|
||||
->label('Facebook Client ID')
|
||||
@ -219,7 +219,7 @@ class ManageGeneralSettings extends SettingsPage
|
||||
->nullable()
|
||||
->maxLength(255),
|
||||
Toggle::make('enable_apple_login')
|
||||
->label('Apple ile Giriş Aktif')
|
||||
->label('Apple Login Enabled')
|
||||
->default($defaults['enable_apple_login']),
|
||||
TextInput::make('apple_client_id')
|
||||
->label('Apple Client ID')
|
||||
@ -241,13 +241,13 @@ class ManageGeneralSettings extends SettingsPage
|
||||
|
||||
return [
|
||||
'site_name' => $siteName,
|
||||
'site_description' => 'Alim satim icin hizli ve guvenli ilan platformu.',
|
||||
'site_description' => 'A fast and secure marketplace for buying and selling.',
|
||||
'media_disk' => MediaStorage::defaultDriver(),
|
||||
'home_slides' => $this->defaultHomeSlides(),
|
||||
'site_logo_disk' => null,
|
||||
'sender_name' => $siteName,
|
||||
'sender_email' => (string) config('mail.from.address', 'info@' . $siteHost),
|
||||
'default_language' => in_array(config('app.locale'), array_keys($this->localeOptions()), true) ? (string) config('app.locale') : 'tr',
|
||||
'default_language' => in_array(config('app.locale'), array_keys($this->localeOptions()), true) ? (string) config('app.locale') : 'en',
|
||||
'default_country_code' => CountryCodeManager::normalizeCountryCode(config('app.default_country_code', '+90')),
|
||||
'currencies' => $this->normalizeCurrencies(config('app.currencies', ['TRY'])),
|
||||
'linkedin_url' => 'https://www.linkedin.com/company/openclassify',
|
||||
@ -264,7 +264,7 @@ class ManageGeneralSettings extends SettingsPage
|
||||
{
|
||||
$labels = [
|
||||
'en' => 'English',
|
||||
'tr' => 'Türkçe',
|
||||
'tr' => 'Turkish',
|
||||
];
|
||||
|
||||
return collect(config('app.available_locales', ['en']))
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Category\Http\Controllers\CategoryController;
|
||||
|
||||
Route::middleware('web')->group(function () {
|
||||
Route::prefix('categories')->name('categories.')->group(function () {
|
||||
Route::get('/', [CategoryController::class, 'index'])->name('index');
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,11 +3,13 @@
|
||||
namespace Modules\Conversation\App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Conversation\App\Models\Conversation;
|
||||
use Modules\Conversation\App\Models\ConversationMessage;
|
||||
use Modules\Conversation\App\Support\QuickMessageCatalog;
|
||||
use Modules\Listing\Models\Listing;
|
||||
use Throwable;
|
||||
@ -56,28 +58,45 @@ class ConversationController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function start(Request $request, Listing $listing): RedirectResponse
|
||||
public function start(Request $request, Listing $listing): RedirectResponse | JsonResponse
|
||||
{
|
||||
if (! $this->messagingTablesReady()) {
|
||||
return back()->with('error', 'Mesajlaşma altyapısı henüz hazır değil.');
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['message' => 'Messaging is not available yet.'], 503);
|
||||
}
|
||||
|
||||
return back()->with('error', 'Messaging is not available yet.');
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if (! $listing->user_id) {
|
||||
return back()->with('error', 'Bu ilan için mesajlaşma açılamadı.');
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['message' => 'A conversation cannot be started for this listing.'], 422);
|
||||
}
|
||||
|
||||
return back()->with('error', 'A conversation cannot be started for this listing.');
|
||||
}
|
||||
|
||||
if ((int) $listing->user_id === (int) $user->getKey()) {
|
||||
return back()->with('error', 'Kendi ilanına mesaj gönderemezsin.');
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['message' => 'You cannot message your own listing.'], 422);
|
||||
}
|
||||
|
||||
return back()->with('error', 'You cannot message your own listing.');
|
||||
}
|
||||
|
||||
$messageBody = trim((string) $request->string('message'));
|
||||
|
||||
if ($request->expectsJson() && $messageBody === '') {
|
||||
return response()->json(['message' => 'Message cannot be empty.'], 422);
|
||||
}
|
||||
|
||||
$conversation = Conversation::openForListingBuyer($listing, (int) $user->getKey());
|
||||
|
||||
$user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]);
|
||||
|
||||
$messageBody = trim((string) $request->string('message'));
|
||||
|
||||
$message = null;
|
||||
if ($messageBody !== '') {
|
||||
$message = $conversation->messages()->create([
|
||||
'sender_id' => $user->getKey(),
|
||||
@ -87,15 +106,23 @@ class ConversationController extends Controller
|
||||
$conversation->forceFill(['last_message_at' => $message->created_at])->save();
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()]))
|
||||
->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.');
|
||||
if ($request->expectsJson()) {
|
||||
return $this->conversationJsonResponse($conversation, $message, (int) $user->getKey());
|
||||
}
|
||||
|
||||
public function send(Request $request, Conversation $conversation): RedirectResponse
|
||||
return redirect()
|
||||
->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()]))
|
||||
->with('success', $messageBody !== '' ? 'Message sent.' : 'Conversation started.');
|
||||
}
|
||||
|
||||
public function send(Request $request, Conversation $conversation): RedirectResponse | JsonResponse
|
||||
{
|
||||
if (! $this->messagingTablesReady()) {
|
||||
return back()->with('error', 'Mesajlaşma altyapısı henüz hazır değil.');
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['message' => 'Messaging is not available yet.'], 503);
|
||||
}
|
||||
|
||||
return back()->with('error', 'Messaging is not available yet.');
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
@ -112,7 +139,11 @@ class ConversationController extends Controller
|
||||
$messageBody = trim($payload['message']);
|
||||
|
||||
if ($messageBody === '') {
|
||||
return back()->with('error', 'Mesaj boş olamaz.');
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['message' => 'Message cannot be empty.'], 422);
|
||||
}
|
||||
|
||||
return back()->with('error', 'Message cannot be empty.');
|
||||
}
|
||||
|
||||
$message = $conversation->messages()->create([
|
||||
@ -122,9 +153,32 @@ class ConversationController extends Controller
|
||||
|
||||
$conversation->forceFill(['last_message_at' => $message->created_at])->save();
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
return $this->conversationJsonResponse($conversation, $message, $userId);
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()]))
|
||||
->with('success', 'Mesaj gönderildi.');
|
||||
->with('success', 'Message sent.');
|
||||
}
|
||||
|
||||
private function conversationJsonResponse(Conversation $conversation, ?ConversationMessage $message, int $userId): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'conversation_id' => (int) $conversation->getKey(),
|
||||
'send_url' => route('conversations.messages.send', $conversation),
|
||||
'message' => $message ? $this->messagePayload($message, $userId) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
private function messagePayload(ConversationMessage $message, int $userId): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) $message->getKey(),
|
||||
'body' => (string) $message->body,
|
||||
'time' => $message->created_at?->format('H:i') ?? now()->format('H:i'),
|
||||
'is_mine' => (int) $message->sender_id === $userId,
|
||||
];
|
||||
}
|
||||
|
||||
private function inboxFilters(Request $request): array
|
||||
|
||||
@ -7,10 +7,10 @@ class QuickMessageCatalog
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
'Merhaba',
|
||||
'İlan hâlâ satışta mı?',
|
||||
'Son fiyat nedir?',
|
||||
'Teşekkürler',
|
||||
'Hi',
|
||||
'Is this listing still available?',
|
||||
'What is your best price?',
|
||||
'Thanks',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,44 +1,44 @@
|
||||
@extends('app::layouts.app')
|
||||
|
||||
@section('title', 'Gelen Kutusu')
|
||||
@section('title', 'Inbox')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
||||
@include('panel.partials.sidebar', ['activeMenu' => 'inbox'])
|
||||
|
||||
<section class="bg-white border border-slate-200 rounded-xl p-0 overflow-hidden">
|
||||
<section class="space-y-4">
|
||||
@include('panel.partials.page-header', [
|
||||
'title' => 'Inbox',
|
||||
'description' => 'Read and reply to buyer messages from the same panel shell used across the site.',
|
||||
'actions' => $requiresLogin ?? false
|
||||
? new \Illuminate\Support\HtmlString('<a href="' . e(route('login', ['redirect' => request()->fullUrl()])) . '" class="inline-flex items-center justify-center rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-white transition hover:bg-slate-800">Log in</a>')
|
||||
: null,
|
||||
])
|
||||
|
||||
<div class="panel-surface overflow-hidden p-0">
|
||||
@if($requiresLogin ?? false)
|
||||
<div class="border-b border-slate-200 px-5 py-4 bg-slate-50 flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="border-b border-slate-200 px-5 py-4 bg-slate-50">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Inbox</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Stay on this page and log in when you want to access your conversations.</p>
|
||||
<p class="mt-1 text-sm text-slate-500">Stay on this page and log in when you want to access your conversations.</p>
|
||||
</div>
|
||||
<a href="{{ route('login', ['redirect' => request()->fullUrl()]) }}" class="inline-flex items-center justify-center rounded-full bg-slate-900 px-5 py-2.5 text-sm font-semibold text-white hover:bg-slate-800 transition">
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-[420px,1fr] min-h-[620px]">
|
||||
<div class="border-b xl:border-b-0 xl:border-r border-slate-200">
|
||||
<div class="px-6 py-5 border-b border-slate-200 flex items-center justify-between gap-3">
|
||||
<h1 class="text-3xl font-bold text-slate-900">Gelen Kutusu</h1>
|
||||
<svg class="w-6 h-6 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35m1.6-5.05a7.25 7.25 0 11-14.5 0 7.25 7.25 0 0114.5 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="px-6 py-4 border-b border-slate-200">
|
||||
<p class="text-sm font-semibold text-slate-600 mb-2">Hızlı Filtreler</p>
|
||||
<p class="mb-2 text-sm font-semibold text-slate-600">Filters</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="{{ route('panel.inbox.index', ['message_filter' => 'all']) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold border {{ $messageFilter === 'all' ? 'border-rose-400 bg-rose-50 text-rose-600' : 'border-slate-300 text-slate-600 hover:bg-slate-100' }}">
|
||||
Hepsi
|
||||
All
|
||||
</a>
|
||||
<a href="{{ route('panel.inbox.index', ['message_filter' => 'unread']) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold border {{ $messageFilter === 'unread' ? 'border-rose-400 bg-rose-50 text-rose-600' : 'border-slate-300 text-slate-600 hover:bg-slate-100' }}">
|
||||
Okunmamış
|
||||
Unread
|
||||
</a>
|
||||
<a href="{{ route('panel.inbox.index', ['message_filter' => 'important']) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold border {{ $messageFilter === 'important' ? 'border-rose-400 bg-rose-50 text-rose-600' : 'border-slate-300 text-slate-600 hover:bg-slate-100' }}">
|
||||
Önemli
|
||||
Important
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -57,17 +57,17 @@
|
||||
@if($conversationImage)
|
||||
<img src="{{ $conversationImage }}" alt="{{ $conversationListing?->title }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="w-full h-full grid place-items-center text-slate-400 text-xs">İlan</div>
|
||||
<div class="w-full h-full grid place-items-center text-slate-400 text-xs">Listing</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-2">
|
||||
<p class="font-semibold text-2xl text-slate-900 truncate">{{ $partner?->name ?? 'Kullanıcı' }}</p>
|
||||
<p class="font-semibold text-2xl text-slate-900 truncate">{{ $partner?->name ?? 'User' }}</p>
|
||||
<p class="text-xs text-slate-500 whitespace-nowrap ml-auto">{{ $conversation->last_message_at?->format('d.m.Y') }}</p>
|
||||
</div>
|
||||
<p class="text-sm text-slate-500 truncate mt-1">{{ $conversationListing?->title ?? 'İlan silinmiş' }}</p>
|
||||
<p class="text-sm text-slate-500 truncate mt-1">{{ $conversationListing?->title ?? 'Listing removed' }}</p>
|
||||
<p class="text-sm {{ $conversation->unread_count > 0 ? 'text-slate-900 font-semibold' : 'text-slate-500' }} truncate mt-1">
|
||||
{{ $lastMessage !== '' ? $lastMessage : 'Henüz mesaj yok' }}
|
||||
{{ $lastMessage !== '' ? $lastMessage : 'No messages yet' }}
|
||||
</p>
|
||||
</div>
|
||||
@if($conversation->unread_count > 0)
|
||||
@ -79,7 +79,7 @@
|
||||
</a>
|
||||
@empty
|
||||
<div class="px-6 py-16 text-center text-slate-500">
|
||||
Henüz bir sohbetin yok.
|
||||
No conversations yet.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
@ -101,8 +101,8 @@
|
||||
{{ strtoupper(substr((string) ($activePartner?->name ?? 'K'), 0, 1)) }}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-3xl font-bold text-slate-900 truncate">{{ $activePartner?->name ?? 'Kullanıcı' }}</p>
|
||||
<p class="text-sm text-slate-500 truncate">{{ $activeListing?->title ?? 'İlan silinmiş' }}</p>
|
||||
<p class="text-3xl font-bold text-slate-900 truncate">{{ $activePartner?->name ?? 'User' }}</p>
|
||||
<p class="text-sm text-slate-500 truncate">{{ $activeListing?->title ?? 'Listing removed' }}</p>
|
||||
</div>
|
||||
@if($activePriceLabel)
|
||||
<div class="ml-auto text-3xl font-semibold text-slate-800 whitespace-nowrap">{{ $activePriceLabel }}</div>
|
||||
@ -125,8 +125,8 @@
|
||||
@empty
|
||||
<div class="h-full grid place-items-center text-slate-500 text-center px-8">
|
||||
<div>
|
||||
<p class="font-semibold text-slate-700">Henüz mesaj yok.</p>
|
||||
<p class="text-sm mt-1">Aşağıdaki hazır metinlerden birini seçebilir veya yeni mesaj yazabilirsin.</p>
|
||||
<p class="font-semibold text-slate-700">No messages yet.</p>
|
||||
<p class="mt-1 text-sm">Use a quick reply or send the first message below.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
@ -148,8 +148,8 @@
|
||||
<form method="POST" action="{{ route('conversations.messages.send', $selectedConversation) }}" class="flex items-center gap-2 border-t border-slate-200 pt-3 mt-1">
|
||||
@csrf
|
||||
<input type="hidden" name="message_filter" value="{{ $messageFilter }}">
|
||||
<input type="text" name="message" value="{{ old('message') }}" placeholder="Bir mesaj yaz" maxlength="2000" class="h-12 flex-1 rounded-full border border-slate-300 px-5 text-sm focus:outline-none focus:ring-2 focus:ring-rose-300" required>
|
||||
<button type="submit" class="h-12 w-12 rounded-full bg-black text-white grid place-items-center hover:bg-slate-800 transition" aria-label="Gönder">
|
||||
<input type="text" name="message" value="{{ old('message') }}" placeholder="Write a message" maxlength="2000" class="h-12 flex-1 rounded-full border border-slate-300 px-5 text-sm focus:outline-none focus:ring-2 focus:ring-rose-300" required>
|
||||
<button type="submit" class="h-12 w-12 rounded-full bg-black text-white grid place-items-center hover:bg-slate-800 transition" aria-label="Send">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h13m0 0l-5-5m5 5l-5 5"/>
|
||||
</svg>
|
||||
@ -162,13 +162,14 @@
|
||||
@else
|
||||
<div class="h-full min-h-[620px] grid place-items-center px-8 text-center text-slate-500">
|
||||
<div>
|
||||
<p class="text-2xl font-semibold text-slate-700">Mesajlaşma için bir sohbet seç.</p>
|
||||
<p class="mt-2 text-sm">İlan detayından veya ilan kartlarından yeni sohbet başlatabilirsin.</p>
|
||||
<p class="text-2xl font-semibold text-slate-700">Choose a conversation to start messaging.</p>
|
||||
<p class="mt-2 text-sm">Start a new chat from a listing detail page or continue an existing thread here.</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Conversation\App\Http\Controllers\ConversationController;
|
||||
|
||||
Route::middleware('web')->group(function () {
|
||||
Route::prefix('panel')->name('panel.')->group(function () {
|
||||
Route::get('/inbox', [ConversationController::class, 'inbox'])->name('inbox.index');
|
||||
});
|
||||
@ -11,3 +12,4 @@ Route::middleware('auth')->name('conversations.')->group(function () {
|
||||
Route::post('/listings/{listing}/conversation', [ConversationController::class, 'start'])->name('start');
|
||||
Route::post('/conversations/{conversation}/messages', [ConversationController::class, 'send'])->name('messages.send');
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,10 +11,15 @@ use Modules\Conversation\App\Models\Conversation;
|
||||
use Modules\Favorite\App\Models\FavoriteSearch;
|
||||
use Modules\Listing\Models\Listing;
|
||||
use Modules\User\App\Models\User;
|
||||
use Modules\User\App\Support\AuthRedirector;
|
||||
use Throwable;
|
||||
|
||||
class FavoriteController extends Controller
|
||||
{
|
||||
public function __construct(private AuthRedirector $redirector)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$activeTab = (string) $request->string('tab', 'listings');
|
||||
@ -126,7 +131,7 @@ class FavoriteController extends Controller
|
||||
{
|
||||
$isNowFavorite = $request->user()->toggleFavoriteListing($listing);
|
||||
|
||||
return back()->with('success', $isNowFavorite ? 'İlan favorilere eklendi.' : 'İlan favorilerden kaldırıldı.');
|
||||
return $this->redirectBack($request)->with('success', $isNowFavorite ? 'Listing added to favorites.' : 'Listing removed from favorites.');
|
||||
}
|
||||
|
||||
public function toggleSeller(Request $request, User $seller)
|
||||
@ -134,12 +139,12 @@ class FavoriteController extends Controller
|
||||
$user = $request->user();
|
||||
|
||||
if ((int) $user->getKey() === (int) $seller->getKey()) {
|
||||
return back()->with('error', 'Kendi hesabını favorilere ekleyemezsin.');
|
||||
return $this->redirectBack($request)->with('error', 'You cannot favorite your own account.');
|
||||
}
|
||||
|
||||
$isNowFavorite = $user->toggleFavoriteSeller($seller);
|
||||
|
||||
return back()->with('success', $isNowFavorite ? 'Satıcı favorilere eklendi.' : 'Satıcı favorilerden kaldırıldı.');
|
||||
return $this->redirectBack($request)->with('success', $isNowFavorite ? 'Seller added to favorites.' : 'Seller removed from favorites.');
|
||||
}
|
||||
|
||||
public function storeSearch(Request $request)
|
||||
@ -155,7 +160,7 @@ class FavoriteController extends Controller
|
||||
]);
|
||||
|
||||
if ($filters === []) {
|
||||
return back()->with('error', 'Favoriye eklemek için en az bir filtre seçmelisin.');
|
||||
return back()->with('error', 'Select at least one filter before saving a search.');
|
||||
}
|
||||
|
||||
$signature = FavoriteSearch::signatureFor($filters);
|
||||
@ -178,10 +183,10 @@ class FavoriteController extends Controller
|
||||
);
|
||||
|
||||
if (! $favoriteSearch->wasRecentlyCreated) {
|
||||
return back()->with('success', 'Bu arama zaten favorilerinde.');
|
||||
return back()->with('success', 'This search is already in your favorites.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Arama favorilere eklendi.');
|
||||
return back()->with('success', 'Search added to favorites.');
|
||||
}
|
||||
|
||||
public function destroySearch(Request $request, FavoriteSearch $favoriteSearch)
|
||||
@ -192,7 +197,7 @@ class FavoriteController extends Controller
|
||||
|
||||
$favoriteSearch->delete();
|
||||
|
||||
return back()->with('success', 'Favori arama silindi.');
|
||||
return back()->with('success', 'Saved search deleted.');
|
||||
}
|
||||
|
||||
private function tableExists(string $table): bool
|
||||
@ -211,4 +216,15 @@ class FavoriteController extends Controller
|
||||
'query' => request()->query(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function redirectBack(Request $request): \Illuminate\Http\RedirectResponse
|
||||
{
|
||||
$target = $this->redirector->sanitize((string) $request->input('redirect_to', ''));
|
||||
|
||||
if ($target !== null) {
|
||||
return redirect()->to($target);
|
||||
}
|
||||
|
||||
return back();
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +51,6 @@ class FavoriteSearch extends Model
|
||||
$labelParts[] = $categoryName;
|
||||
}
|
||||
|
||||
return $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtreli arama';
|
||||
return $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtered search';
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
@extends('app::layouts.app')
|
||||
|
||||
@section('title', 'Favoriler')
|
||||
@section('title', 'Favorites')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||
@ -29,25 +29,25 @@
|
||||
], fn ($value) => !is_null($value) && $value !== '');
|
||||
@endphp
|
||||
<div class="border-b-2 border-blue-900 px-4 py-3 flex flex-wrap items-center gap-3">
|
||||
<h1 class="text-3xl font-bold text-slate-800 mr-auto">Favori Listem</h1>
|
||||
<h1 class="text-3xl font-bold text-slate-800 mr-auto">Saved Listings</h1>
|
||||
<div class="inline-flex border border-slate-300 overflow-hidden">
|
||||
<a href="{{ route('favorites.index', array_merge($listingTabQuery, ['status' => 'all'])) }}" class="px-5 py-2 text-sm font-semibold {{ $statusFilter === 'all' ? 'bg-slate-700 text-white' : 'bg-white text-slate-700 hover:bg-slate-100' }}">
|
||||
Tümü
|
||||
All
|
||||
</a>
|
||||
<a href="{{ route('favorites.index', array_merge($listingTabQuery, ['status' => 'active'])) }}" class="px-5 py-2 text-sm font-semibold border-l border-slate-300 {{ $statusFilter === 'active' ? 'bg-slate-700 text-white' : 'bg-white text-slate-700 hover:bg-slate-100' }}">
|
||||
Yayında
|
||||
Live
|
||||
</a>
|
||||
</div>
|
||||
<form method="GET" action="{{ route('favorites.index') }}" class="flex items-center gap-2">
|
||||
<input type="hidden" name="tab" value="listings">
|
||||
<input type="hidden" name="status" value="{{ $statusFilter }}">
|
||||
<select name="category" class="h-10 min-w-44 border border-slate-300 px-3 text-sm text-slate-700">
|
||||
<option value="">Kategori</option>
|
||||
<option value="">Category</option>
|
||||
@foreach($categories as $category)
|
||||
<option value="{{ $category->id }}" @selected((int) $selectedCategoryId === (int) $category->id)>{{ $category->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="h-10 px-4 bg-slate-700 text-white text-sm font-semibold hover:bg-slate-800 transition">Filtrele</button>
|
||||
<button type="submit" class="h-10 px-4 bg-slate-700 text-white text-sm font-semibold hover:bg-slate-800 transition">Filter</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -55,9 +55,9 @@
|
||||
<table class="w-full min-w-[860px]">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 text-slate-700 text-sm">
|
||||
<th class="text-left px-4 py-3 w-[58%]">İlan Başlığı</th>
|
||||
<th class="text-left px-4 py-3 w-[16%]">Fiyat</th>
|
||||
<th class="text-left px-4 py-3 w-[14%]">Mesajlaşma</th>
|
||||
<th class="text-left px-4 py-3 w-[58%]">Listing</th>
|
||||
<th class="text-left px-4 py-3 w-[16%]">Price</th>
|
||||
<th class="text-left px-4 py-3 w-[14%]">Messaging</th>
|
||||
<th class="text-right px-4 py-3 w-[12%]"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -65,7 +65,7 @@
|
||||
@forelse($favoriteListings as $listing)
|
||||
@php
|
||||
$listingImage = $listing->getFirstMediaUrl('listing-images');
|
||||
$priceLabel = $listing->price ? number_format((float) $listing->price, 0).' '.$listing->currency : 'Ücretsiz';
|
||||
$priceLabel = $listing->price ? number_format((float) $listing->price, 0).' '.$listing->currency : 'Free';
|
||||
$meta = collect([
|
||||
$listing->category?->name,
|
||||
$listing->city,
|
||||
@ -82,15 +82,15 @@
|
||||
@if($listingImage)
|
||||
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover">
|
||||
@else
|
||||
<div class="w-full h-full grid place-items-center text-slate-400">Görsel yok</div>
|
||||
<div class="w-full h-full grid place-items-center text-slate-400">No image</div>
|
||||
@endif
|
||||
</a>
|
||||
<div>
|
||||
<a href="{{ route('listings.show', $listing) }}" class="font-semibold text-2xl text-slate-800 hover:text-blue-700 transition leading-6">
|
||||
{{ $listing->title }}
|
||||
</a>
|
||||
<p class="text-sm text-slate-500 mt-2">{{ $meta !== '' ? $meta : 'Kategori / konum bilgisi yok' }}</p>
|
||||
<p class="text-xs text-slate-400 mt-1">Favoriye eklenme: {{ $listing->pivot->created_at?->format('d.m.Y') }}</p>
|
||||
<p class="text-sm text-slate-500 mt-2">{{ $meta !== '' ? $meta : 'No category or location data' }}</p>
|
||||
<p class="text-xs text-slate-400 mt-1">Saved on: {{ $listing->pivot->created_at?->format('M j, Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -99,31 +99,31 @@
|
||||
@if($canMessageListing)
|
||||
@if($conversationId)
|
||||
<a href="{{ route('panel.inbox.index', ['conversation' => $conversationId]) }}" class="inline-flex items-center h-10 px-4 border border-rose-300 text-rose-600 text-sm font-semibold rounded-full hover:bg-rose-50 transition">
|
||||
Sohbete Git
|
||||
Open chat
|
||||
</a>
|
||||
@else
|
||||
<form method="POST" action="{{ route('conversations.start', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center h-10 px-4 bg-rose-500 text-white text-sm font-semibold rounded-full hover:bg-rose-600 transition">
|
||||
Mesaj Gönder
|
||||
Send message
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-xs text-slate-400">{{ $isOwnListing ? 'Kendi ilanın' : 'Satıcı bilgisi yok' }}</span>
|
||||
<span class="text-xs text-slate-400">{{ $isOwnListing ? 'Your own listing' : 'Seller unavailable' }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-4 text-right">
|
||||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="text-sm font-semibold text-rose-500 hover:text-rose-600 transition">Kaldır</button>
|
||||
<button type="submit" class="text-sm font-semibold text-rose-500 hover:text-rose-600 transition">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="border-t border-slate-200">
|
||||
<td colspan="4" class="px-4 py-10 text-center text-slate-500">
|
||||
Henüz favori ilan bulunmuyor.
|
||||
No saved listings yet.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
@ -132,7 +132,7 @@
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 border-t border-slate-200 text-sm text-slate-500">
|
||||
* Son 1 yıl içinde favoriye eklediğiniz ilanlar listelenmektedir.
|
||||
* Listings saved within the last year are shown here.
|
||||
</div>
|
||||
|
||||
@if($favoriteListings?->hasPages())
|
||||
@ -142,8 +142,8 @@
|
||||
|
||||
@if($activeTab === 'searches')
|
||||
<div class="px-4 py-4 border-b border-slate-200">
|
||||
<h1 class="text-3xl font-bold text-slate-800">Favori Aramalar</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Kayıtlı aramalarına tek tıkla geri dön.</p>
|
||||
<h1 class="text-3xl font-bold text-slate-800">Saved Searches</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Return to your saved searches with one click.</p>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-200">
|
||||
@forelse($favoriteSearches as $favoriteSearch)
|
||||
@ -155,29 +155,29 @@
|
||||
@endphp
|
||||
<article class="px-4 py-4 flex flex-col md:flex-row md:items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<h2 class="font-semibold text-slate-800">{{ $favoriteSearch->label ?: 'Kayıtlı arama' }}</h2>
|
||||
<h2 class="font-semibold text-slate-800">{{ $favoriteSearch->label ?: 'Saved search' }}</h2>
|
||||
<p class="text-sm text-slate-500 mt-1">
|
||||
@if($favoriteSearch->search_term) Arama: "{{ $favoriteSearch->search_term }}" · @endif
|
||||
@if($favoriteSearch->category) Kategori: {{ $favoriteSearch->category->name }} · @endif
|
||||
Kaydedilme: {{ $favoriteSearch->created_at?->format('d.m.Y H:i') }}
|
||||
@if($favoriteSearch->search_term) Search: "{{ $favoriteSearch->search_term }}" · @endif
|
||||
@if($favoriteSearch->category) Category: {{ $favoriteSearch->category->name }} · @endif
|
||||
Saved: {{ $favoriteSearch->created_at?->format('M j, Y H:i') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ $searchUrl }}" class="inline-flex items-center h-10 px-4 bg-blue-600 text-white text-sm font-semibold rounded hover:bg-blue-700 transition">
|
||||
Aramayı Aç
|
||||
Open search
|
||||
</a>
|
||||
<form method="POST" action="{{ route('favorites.searches.destroy', $favoriteSearch) }}">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="inline-flex items-center h-10 px-4 border border-slate-300 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition">
|
||||
Sil
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
@empty
|
||||
<div class="px-4 py-10 text-center text-slate-500">
|
||||
Henüz favori arama eklenmedi.
|
||||
No saved searches yet.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
@ -188,32 +188,32 @@
|
||||
|
||||
@if($activeTab === 'sellers')
|
||||
<div class="px-4 py-4 border-b border-slate-200">
|
||||
<h1 class="text-3xl font-bold text-slate-800">Favori Satıcılar</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Takip etmek istediğin satıcıları burada yönetebilirsin.</p>
|
||||
<h1 class="text-3xl font-bold text-slate-800">Saved Sellers</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Manage the sellers you want to follow here.</p>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-200">
|
||||
@forelse($favoriteSellers as $seller)
|
||||
<article class="px-4 py-4 flex flex-col md:flex-row md:items-center gap-3">
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<a href="{{ route('listings.index', ['user' => $seller->id]) }}" class="flex items-center gap-3 flex-1 hover:opacity-90 transition">
|
||||
<div class="w-12 h-12 rounded-full bg-blue-100 text-blue-700 font-bold grid place-items-center">
|
||||
{{ strtoupper(substr((string) $seller->name, 0, 1)) }}
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-semibold text-slate-800">{{ $seller->name }}</h2>
|
||||
<p class="text-sm text-slate-500">{{ $seller->email }}</p>
|
||||
<p class="text-xs text-slate-400 mt-1">Aktif ilan: {{ (int) $seller->active_listings_count }}</p>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1">Active listings: {{ (int) $seller->active_listings_count }}</p>
|
||||
</div>
|
||||
</a>
|
||||
<form method="POST" action="{{ route('favorites.sellers.toggle', $seller) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center h-10 px-4 border border-rose-200 text-sm font-semibold text-rose-600 hover:bg-rose-50 transition">
|
||||
Favoriden Kaldır
|
||||
Remove seller
|
||||
</button>
|
||||
</form>
|
||||
</article>
|
||||
@empty
|
||||
<div class="px-4 py-10 text-center text-slate-500">
|
||||
Henüz favori satıcı eklenmedi.
|
||||
No saved sellers yet.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Favorite\App\Http\Controllers\FavoriteController;
|
||||
|
||||
Route::middleware('web')->group(function () {
|
||||
Route::prefix('favorites')->name('favorites.')->group(function () {
|
||||
Route::get('/', [FavoriteController::class, 'index'])->name('index');
|
||||
});
|
||||
@ -13,3 +14,4 @@ Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(functi
|
||||
Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store');
|
||||
Route::delete('/searches/{favoriteSearch}', [FavoriteController::class, 'destroySearch'])->name('searches.destroy');
|
||||
});
|
||||
});
|
||||
|
||||
@ -25,13 +25,13 @@ class ListingSeeder extends Seeder
|
||||
];
|
||||
|
||||
private const TITLE_PREFIXES = [
|
||||
'Temiz kullanılmış',
|
||||
'Az kullanılmış',
|
||||
'Fırsat ürün',
|
||||
'Uygun fiyatlı',
|
||||
'Sahibinden',
|
||||
'Kaçırılmayacak',
|
||||
'Bakımlı',
|
||||
'Clean',
|
||||
'Lightly used',
|
||||
'Special offer',
|
||||
'Well priced',
|
||||
'Owner listed',
|
||||
'Must-see',
|
||||
'Well kept',
|
||||
];
|
||||
|
||||
public function run(): void
|
||||
@ -100,7 +100,7 @@ class ListingSeeder extends Seeder
|
||||
private function resolveTurkeyCities(): Collection
|
||||
{
|
||||
if (! class_exists(City::class) || ! Schema::hasTable('cities') || ! Schema::hasTable('countries')) {
|
||||
return collect(['İstanbul', 'Ankara', 'İzmir', 'Bursa', 'Antalya']);
|
||||
return collect(['Istanbul', 'Ankara', 'Izmir', 'Bursa', 'Antalya']);
|
||||
}
|
||||
|
||||
$turkey = Country::query()
|
||||
@ -108,7 +108,7 @@ class ListingSeeder extends Seeder
|
||||
->first(['id']);
|
||||
|
||||
if (! $turkey) {
|
||||
return collect(['İstanbul', 'Ankara', 'İzmir', 'Bursa', 'Antalya']);
|
||||
return collect(['Istanbul', 'Ankara', 'Izmir', 'Bursa', 'Antalya']);
|
||||
}
|
||||
|
||||
$cities = City::query()
|
||||
@ -122,7 +122,7 @@ class ListingSeeder extends Seeder
|
||||
|
||||
return $cities->isNotEmpty()
|
||||
? $cities
|
||||
: collect(['İstanbul', 'Ankara', 'İzmir', 'Bursa', 'Antalya']);
|
||||
: collect(['Istanbul', 'Ankara', 'Izmir', 'Bursa', 'Antalya']);
|
||||
}
|
||||
|
||||
private function buildListingData(
|
||||
@ -147,7 +147,7 @@ class ListingSeeder extends Seeder
|
||||
private function resolveLocation(int $index, Collection $countries, Collection $turkeyCities): array
|
||||
{
|
||||
$turkeyCountry = $countries->first(fn ($country): bool => strtoupper((string) $country->code) === 'TR');
|
||||
$turkeyName = trim((string) ($turkeyCountry->name ?? 'Türkiye')) ?: 'Türkiye';
|
||||
$turkeyName = trim((string) ($turkeyCountry->name ?? 'Turkey')) ?: 'Turkey';
|
||||
|
||||
$useForeignCountry = $countries->count() > 1 && $index % 4 === 0;
|
||||
|
||||
@ -161,8 +161,8 @@ class ListingSeeder extends Seeder
|
||||
$countryName = trim((string) ($selected->name ?? ''));
|
||||
|
||||
return [
|
||||
'country' => $countryName !== '' ? $countryName : 'Türkiye',
|
||||
'city' => $countryName !== '' ? $countryName : 'İstanbul',
|
||||
'country' => $countryName !== '' ? $countryName : 'Turkey',
|
||||
'city' => $countryName !== '' ? $countryName : 'Istanbul',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -171,7 +171,7 @@ class ListingSeeder extends Seeder
|
||||
|
||||
return [
|
||||
'country' => $turkeyName,
|
||||
'city' => $city !== '' ? $city : 'İstanbul',
|
||||
'city' => $city !== '' ? $city : 'Istanbul',
|
||||
];
|
||||
}
|
||||
|
||||
@ -180,7 +180,7 @@ class ListingSeeder extends Seeder
|
||||
$prefix = self::TITLE_PREFIXES[$index % count(self::TITLE_PREFIXES)];
|
||||
$categoryName = trim((string) $category->name);
|
||||
|
||||
return sprintf('%s %s ilanı', $prefix, $categoryName !== '' ? $categoryName : 'ürün');
|
||||
return sprintf('%s %s listing', $prefix, $categoryName !== '' ? $categoryName : 'item');
|
||||
}
|
||||
|
||||
private function buildDescription(Category $category, string $city, string $country): string
|
||||
@ -189,9 +189,9 @@ class ListingSeeder extends Seeder
|
||||
$location = trim(collect([$city, $country])->filter()->join(', '));
|
||||
|
||||
return sprintf(
|
||||
'%s kategorisinde, durum olarak sorunsuz ve kullanıma hazırdır. Teslimat noktası: %s. Detaylar için mesaj atabilirsiniz.',
|
||||
$categoryName !== '' ? $categoryName : 'Ürün',
|
||||
$location !== '' ? $location : 'Türkiye'
|
||||
'Listed in %s, in clean condition and ready to use. Pickup area: %s. Message for more details.',
|
||||
$categoryName !== '' ? $categoryName : 'Item',
|
||||
$location !== '' ? $location : 'Turkey'
|
||||
);
|
||||
}
|
||||
|
||||
@ -244,7 +244,7 @@ class ListingSeeder extends Seeder
|
||||
|
||||
if (! is_file($imageAbsolutePath)) {
|
||||
if ($this->command) {
|
||||
$this->command->warn("Gorsel bulunamadi: {$imageRelativePath}");
|
||||
$this->command->warn("Image not found: {$imageRelativePath}");
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@ -33,6 +33,9 @@ class ListingController extends Controller
|
||||
$cityId = request()->integer('city');
|
||||
$cityId = $cityId > 0 ? $cityId : null;
|
||||
|
||||
$sellerUserId = request()->integer('user');
|
||||
$sellerUserId = $sellerUserId > 0 ? $sellerUserId : null;
|
||||
|
||||
$minPriceInput = trim((string) request('min_price', ''));
|
||||
$maxPriceInput = trim((string) request('max_price', ''));
|
||||
$minPrice = is_numeric($minPriceInput) ? max((float) $minPriceInput, 0) : null;
|
||||
@ -70,6 +73,7 @@ class ListingController extends Controller
|
||||
'search' => $search,
|
||||
'country' => $selectedCountryName,
|
||||
'city' => $selectedCityName,
|
||||
'user_id' => $sellerUserId,
|
||||
'min_price' => $minPrice,
|
||||
'max_price' => $maxPrice,
|
||||
'date_filter' => $dateFilter,
|
||||
@ -136,6 +140,7 @@ class ListingController extends Controller
|
||||
'categoryId',
|
||||
'countryId',
|
||||
'cityId',
|
||||
'sellerUserId',
|
||||
'minPriceInput',
|
||||
'maxPriceInput',
|
||||
'dateFilter',
|
||||
@ -184,6 +189,7 @@ class ListingController extends Controller
|
||||
$isListingFavorited = false;
|
||||
$isSellerFavorited = false;
|
||||
$existingConversationId = null;
|
||||
$detailConversation = null;
|
||||
|
||||
if (auth()->check()) {
|
||||
$userId = (int) auth()->id();
|
||||
@ -205,6 +211,17 @@ class ListingController extends Controller
|
||||
(int) $listing->getKey(),
|
||||
$userId,
|
||||
);
|
||||
|
||||
if ($existingConversationId) {
|
||||
$detailConversation = Conversation::query()
|
||||
->forUser($userId)
|
||||
->find($existingConversationId);
|
||||
|
||||
if ($detailConversation) {
|
||||
$detailConversation->loadThread();
|
||||
$detailConversation->markAsReadFor($userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,6 +231,7 @@ class ListingController extends Controller
|
||||
'isSellerFavorited',
|
||||
'presentableCustomFields',
|
||||
'existingConversationId',
|
||||
'detailConversation',
|
||||
'gallery',
|
||||
'listingVideos',
|
||||
'relatedListings',
|
||||
@ -239,7 +257,7 @@ class ListingController extends Controller
|
||||
|
||||
return redirect()
|
||||
->route('panel.listings.create')
|
||||
->with('success', 'İlan oluşturma ekranına yönlendirildin.');
|
||||
->with('success', 'You were redirected to the listing creation screen.');
|
||||
}
|
||||
|
||||
private function resolveLocationFilters(
|
||||
|
||||
@ -146,6 +146,7 @@ class Listing extends Model implements HasMedia
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
$country = isset($filters['country']) ? trim((string) $filters['country']) : null;
|
||||
$city = isset($filters['city']) ? trim((string) $filters['city']) : null;
|
||||
$userId = isset($filters['user_id']) && is_numeric($filters['user_id']) ? (int) $filters['user_id'] : null;
|
||||
$minPrice = is_numeric($filters['min_price'] ?? null) ? max((float) $filters['min_price'], 0) : null;
|
||||
$maxPrice = is_numeric($filters['max_price'] ?? null) ? max((float) $filters['max_price'], 0) : null;
|
||||
$dateFilter = (string) ($filters['date_filter'] ?? 'all');
|
||||
@ -154,6 +155,7 @@ class Listing extends Model implements HasMedia
|
||||
$query
|
||||
->searchTerm($search)
|
||||
->forCategoryIds(is_array($categoryIds) ? $categoryIds : null)
|
||||
->when(! is_null($userId) && $userId > 0, fn (Builder $builder) => $builder->where('user_id', $userId))
|
||||
->when($country !== null && $country !== '', fn (Builder $builder) => $builder->where('country', $country))
|
||||
->when($city !== null && $city !== '', fn (Builder $builder) => $builder->where('city', $city))
|
||||
->when(! is_null($minPrice), fn (Builder $builder) => $builder->whereNotNull('price')->where('price', '>=', $minPrice))
|
||||
@ -259,7 +261,7 @@ class Listing extends Model implements HasMedia
|
||||
public function panelPriceLabel(): string
|
||||
{
|
||||
if (is_null($this->price)) {
|
||||
return 'Ücretsiz';
|
||||
return 'Free';
|
||||
}
|
||||
|
||||
return number_format((float) $this->price, 2, ',', '.').' '.($this->currency ?? 'TL');
|
||||
@ -269,24 +271,24 @@ class Listing extends Model implements HasMedia
|
||||
{
|
||||
return match ($this->statusValue()) {
|
||||
'sold' => [
|
||||
'label' => 'Satıldı',
|
||||
'label' => 'Sold',
|
||||
'badge_class' => 'is-success',
|
||||
'hint' => 'İlan satıldı olarak işaretlendi.',
|
||||
'hint' => 'This listing is marked as sold.',
|
||||
],
|
||||
'expired' => [
|
||||
'label' => 'Süresi doldu',
|
||||
'label' => 'Expired',
|
||||
'badge_class' => 'is-danger',
|
||||
'hint' => 'Yeniden yayına alınmayı bekliyor.',
|
||||
'hint' => 'This listing is waiting to be republished.',
|
||||
],
|
||||
'pending' => [
|
||||
'label' => 'İncelemede',
|
||||
'label' => 'Pending review',
|
||||
'badge_class' => 'is-warning',
|
||||
'hint' => 'Moderasyon onayı bekleniyor.',
|
||||
'hint' => 'Waiting for moderation approval.',
|
||||
],
|
||||
default => [
|
||||
'label' => 'Yayında',
|
||||
'label' => 'Live',
|
||||
'badge_class' => 'is-primary',
|
||||
'hint' => 'Şu anda ziyaretçilere görünüyor.',
|
||||
'hint' => 'Visible to visitors right now.',
|
||||
],
|
||||
};
|
||||
}
|
||||
@ -298,7 +300,7 @@ class Listing extends Model implements HasMedia
|
||||
trim((string) $this->country),
|
||||
])->filter()->values();
|
||||
|
||||
return $parts->isNotEmpty() ? $parts->implode(', ') : 'Konum belirtilmedi';
|
||||
return $parts->isNotEmpty() ? $parts->implode(', ') : 'Location not specified';
|
||||
}
|
||||
|
||||
public function panelPublishedAt(): ?Carbon
|
||||
@ -320,16 +322,16 @@ class Listing extends Model implements HasMedia
|
||||
public function panelExpirySummary(): string
|
||||
{
|
||||
if (! $this->expires_at) {
|
||||
return 'Süre sınırı yok';
|
||||
return 'No expiry limit';
|
||||
}
|
||||
|
||||
$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',
|
||||
$days > 0 => $days.' days left',
|
||||
$days === 0 => 'Ends today',
|
||||
default => 'Expired '.abs($days).' days ago',
|
||||
};
|
||||
}
|
||||
|
||||
@ -340,8 +342,8 @@ class Listing extends Model implements HasMedia
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => $total.' video',
|
||||
'detail' => $ready.' hazır'.($pending > 0 ? ', '.$pending.' işleniyor' : ''),
|
||||
'label' => $total.' videos',
|
||||
'detail' => $ready.' ready'.($pending > 0 ? ', '.$pending.' processing' : ''),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -66,12 +66,12 @@ class ListingCustomFieldSchemaBuilder
|
||||
$label = $field?->label ?: Str::headline((string) $key);
|
||||
|
||||
if (is_bool($value)) {
|
||||
$displayValue = $value ? 'Evet' : 'Hayır';
|
||||
$displayValue = $value ? 'Yes' : 'No';
|
||||
} elseif (is_array($value)) {
|
||||
$displayValue = implode(', ', array_map(fn ($item): string => (string) $item, $value));
|
||||
} elseif ($field?->type === ListingCustomField::TYPE_DATE) {
|
||||
try {
|
||||
$displayValue = Carbon::parse((string) $value)->format('d.m.Y');
|
||||
$displayValue = Carbon::parse((string) $value)->format('M j, Y');
|
||||
} catch (\Throwable) {
|
||||
$displayValue = (string) $value;
|
||||
}
|
||||
|
||||
@ -1,450 +1,7 @@
|
||||
@extends('app::layouts.app')
|
||||
|
||||
@section('title', trim((string) ($selectedCategory?->name ?? '')) !== '' ? trim((string) $selectedCategory->name).' Listings and Prices' : 'All Listings and Prices')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$allListingsCount = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total();
|
||||
$resultListingsCount = isset($filteredListingsTotal) ? (int) $filteredListingsTotal : (int) $listings->total();
|
||||
$activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : '';
|
||||
$pageTitle = $activeCategoryName !== ''
|
||||
? 'İkinci El '.$activeCategoryName.' İlanları ve Fiyatları'
|
||||
: 'İkinci El Araba İlanları ve Fiyatları';
|
||||
$canSaveSearch = $search !== '' || ! is_null($categoryId);
|
||||
$normalizeQuery = static fn ($value): bool => ! is_null($value) && $value !== '';
|
||||
$baseCategoryQuery = array_filter([
|
||||
'search' => $search !== '' ? $search : null,
|
||||
'country' => $countryId,
|
||||
'city' => $cityId,
|
||||
'min_price' => $minPriceInput !== '' ? $minPriceInput : null,
|
||||
'max_price' => $maxPriceInput !== '' ? $maxPriceInput : null,
|
||||
'date_filter' => $dateFilter !== 'all' ? $dateFilter : null,
|
||||
'sort' => $sort !== 'smart' ? $sort : null,
|
||||
], $normalizeQuery);
|
||||
$clearFiltersQuery = array_filter([
|
||||
'search' => $search !== '' ? $search : null,
|
||||
], $normalizeQuery);
|
||||
@endphp
|
||||
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
|
||||
<h1 class="text-3xl md:text-4xl leading-tight font-bold text-slate-900 mb-6">{{ $pageTitle }}</h1>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[260px,1fr] gap-4 lg:gap-5">
|
||||
<aside class="space-y-4">
|
||||
<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">Kategoriler</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 max-h-[330px] overflow-y-auto pr-1">
|
||||
@php
|
||||
$allCategoriesLink = route('listings.index', $baseCategoryQuery);
|
||||
@endphp
|
||||
<a href="{{ $allCategoriesLink }}" class="flex items-center justify-between rounded-lg px-2.5 py-2 text-sm font-semibold {{ is_null($categoryId) ? 'bg-slate-900 text-white' : 'text-slate-700 hover:bg-slate-100' }}">
|
||||
<span>Tüm İlanlar</span>
|
||||
<span>{{ number_format($allListingsCount, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
|
||||
@foreach($categories as $category)
|
||||
@php
|
||||
$categoryCount = (int) $category->active_listing_total;
|
||||
$isSelectedParent = (int) $categoryId === (int) $category->id;
|
||||
$categoryUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $category->id,
|
||||
]), $normalizeQuery));
|
||||
@endphp
|
||||
<a href="{{ $categoryUrl }}" class="flex items-center justify-between rounded-lg px-2.5 py-2 text-sm font-semibold {{ $isSelectedParent ? 'bg-slate-900 text-white' : 'text-slate-700 hover:bg-slate-100' }}">
|
||||
<span>{{ $category->name }}</span>
|
||||
<span>{{ number_format($categoryCount, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
|
||||
@foreach($category->children as $childCategory)
|
||||
@php
|
||||
$isSelectedChild = (int) $categoryId === (int) $childCategory->id;
|
||||
$childUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $childCategory->id,
|
||||
]), $normalizeQuery));
|
||||
@endphp
|
||||
<a href="{{ $childUrl }}" class="ml-2 flex items-center justify-between rounded-lg px-2 py-1.5 text-[13px] font-medium {{ $isSelectedChild ? 'bg-rose-50 text-rose-600' : 'text-slate-600 hover:bg-slate-100' }}">
|
||||
<span>{{ $childCategory->name }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listing_total, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form method="GET" action="{{ route('listings.index') }}" class="listing-filter-card p-4 space-y-5">
|
||||
@if($search !== '')
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
@endif
|
||||
@if($categoryId)
|
||||
<input type="hidden" name="category" value="{{ $categoryId }}">
|
||||
@endif
|
||||
<input type="hidden" name="sort" value="{{ $sort }}">
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">Konum</h3>
|
||||
<div class="space-y-2.5">
|
||||
@php
|
||||
$citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities')
|
||||
? route('locations.cities', ['country' => '__COUNTRY__'], false)
|
||||
: '';
|
||||
@endphp
|
||||
<select
|
||||
name="country"
|
||||
data-listing-country
|
||||
data-cities-url-template="{{ $citiesRouteTemplate }}"
|
||||
class="w-full h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200"
|
||||
>
|
||||
<option value="">İl seçin</option>
|
||||
@foreach($countries as $country)
|
||||
<option value="{{ $country->id }}" @selected((int) $countryId === (int) $country->id)>
|
||||
{{ $country->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<select name="city" data-listing-city class="w-full h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200" @disabled(!$countryId)>
|
||||
<option value="">{{ $countryId ? 'İlçe seçin' : 'Önce il seçin' }}</option>
|
||||
@foreach($cities as $city)
|
||||
<option value="{{ $city->id }}" @selected((int) $cityId === (int) $city->id)>
|
||||
{{ $city->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<button type="button" data-use-current-location class="w-full h-10 rounded-lg border border-slate-300 bg-white text-sm font-semibold text-slate-700 hover:bg-slate-50 transition">
|
||||
Mevcut konumu kullan
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">Fiyat</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input type="number" name="min_price" value="{{ $minPriceInput }}" min="0" step="1" placeholder="Min" class="h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200">
|
||||
<input type="number" name="max_price" value="{{ $maxPriceInput }}" min="0" step="1" placeholder="Maks" class="h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">İlan Tarihi</h3>
|
||||
<div class="space-y-2 text-sm text-slate-700">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="all" class="accent-rose-500" @checked($dateFilter === 'all')>
|
||||
<span>Tümü</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="today" class="accent-rose-500" @checked($dateFilter === 'today')>
|
||||
<span>Bugün</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="week" class="accent-rose-500" @checked($dateFilter === 'week')>
|
||||
<span>Son 7 Gün</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="month" class="accent-rose-500" @checked($dateFilter === 'month')>
|
||||
<span>Son 30 Gün</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('listings.index', $clearFiltersQuery) }}" class="flex-1 h-10 inline-flex items-center justify-center rounded-full border border-rose-300 text-rose-500 text-sm font-semibold hover:bg-rose-50 transition">
|
||||
Temizle
|
||||
</a>
|
||||
<button type="submit" class="flex-1 h-10 rounded-full bg-rose-500 text-white text-sm font-semibold hover:bg-rose-600 transition">
|
||||
Uygula
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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">
|
||||
<p class="text-sm text-slate-700 mr-auto">
|
||||
{{ $activeCategoryName !== '' ? 'İkinci El '.$activeCategoryName.' kategorisinde' : 'İkinci El Araba kategorisinde' }}
|
||||
<strong>{{ number_format($resultListingsCount, 0, ',', '.') }}</strong>
|
||||
ilan bulundu
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.searches.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
<input type="hidden" name="category_id" value="{{ $categoryId }}">
|
||||
<button type="submit" class="h-10 px-4 rounded-full border text-sm font-semibold transition {{ $isCurrentSearchSaved ? 'bg-emerald-100 border-emerald-200 text-emerald-700 cursor-default' : ($canSaveSearch ? 'bg-rose-50 border-rose-200 text-rose-600 hover:bg-rose-100' : 'bg-slate-100 border-slate-200 text-slate-400 cursor-not-allowed') }}" @disabled($isCurrentSearchSaved || ! $canSaveSearch)>
|
||||
{{ $isCurrentSearchSaved ? 'Arama Kaydedildi' : 'Arama Kaydet' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="h-10 px-4 inline-flex items-center rounded-full border border-slate-300 text-sm font-semibold text-slate-600 hover:bg-slate-50 transition">
|
||||
Arama Kaydet
|
||||
</a>
|
||||
@endauth
|
||||
|
||||
<form method="GET" action="{{ route('listings.index') }}">
|
||||
@if($search !== '')
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
@endif
|
||||
@if($categoryId)
|
||||
<input type="hidden" name="category" value="{{ $categoryId }}">
|
||||
@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="h-10 px-4 rounded-full border border-slate-300 bg-white inline-flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||
<span>Akıllı Sıralama</span>
|
||||
<select name="sort" class="bg-transparent text-sm font-semibold focus:outline-none" onchange="this.form.submit()">
|
||||
<option value="smart" @selected($sort === 'smart')>Önerilen</option>
|
||||
<option value="newest" @selected($sort === 'newest')>En Yeni</option>
|
||||
<option value="oldest" @selected($sort === 'oldest')>En Eski</option>
|
||||
<option value="price_asc" @selected($sort === 'price_asc')>Fiyat Artan</option>
|
||||
<option value="price_desc" @selected($sort === 'price_desc')>Fiyat Azalan</option>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($listings->isEmpty())
|
||||
<div class="listing-filter-card py-14 text-center text-slate-500">
|
||||
Bu filtreye uygun ilan bulunamadı.
|
||||
</div>
|
||||
@else
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3.5">
|
||||
@foreach($listings as $listing)
|
||||
@php
|
||||
$listingImage = $listing->getFirstMediaUrl('listing-images');
|
||||
$isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
|
||||
$priceValue = ! is_null($listing->price) ? (float) $listing->price : null;
|
||||
$locationParts = array_filter([
|
||||
trim((string) ($listing->city ?? '')),
|
||||
trim((string) ($listing->country ?? '')),
|
||||
], fn ($value) => $value !== '');
|
||||
$locationText = implode(', ', $locationParts);
|
||||
@endphp
|
||||
<article class="listing-card">
|
||||
<div class="relative h-52 bg-slate-200">
|
||||
@if($listingImage)
|
||||
<a href="{{ route('listings.show', $listing) }}" class="block w-full h-full">
|
||||
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover">
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('listings.show', $listing) }}" class="w-full h-full grid place-items-center text-slate-400">
|
||||
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 16l4.6-4.6a2 2 0 012.8 0L16 16m-2-2 1.6-1.6a2 2 0 012.8 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($listing->is_featured)
|
||||
<span class="absolute top-2 left-2 inline-flex items-center rounded-full bg-yellow-300 text-slate-900 text-[11px] font-bold px-2.5 py-1">
|
||||
Öne Çıkan
|
||||
</span>
|
||||
@endif
|
||||
|
||||
<div class="absolute top-2 right-2">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="w-8 h-8 rounded-full grid place-items-center transition {{ $isFavorited ? 'bg-rose-500 text-white' : 'bg-white text-slate-500 hover:text-rose-500' }}" aria-label="Favoriye ekle">
|
||||
♥
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="w-8 h-8 rounded-full bg-white text-slate-500 hover:text-rose-500 grid place-items-center transition" aria-label="Giriş yap">
|
||||
♥
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
@if(!is_null($priceValue) && $priceValue > 0)
|
||||
{{ number_format($priceValue, 0, ',', '.') }} {{ $listing->currency }}
|
||||
@else
|
||||
Ücretsiz
|
||||
@endif
|
||||
</p>
|
||||
<h3 class="listing-title mt-2 text-sm font-semibold text-slate-900">
|
||||
{{ $listing->title }}
|
||||
</h3>
|
||||
</a>
|
||||
|
||||
<p class="text-xs text-slate-500 mt-2">
|
||||
{{ $listing->category?->name ?: 'Kategori yok' }}
|
||||
</p>
|
||||
|
||||
<div class="mt-3 pt-2 border-t border-slate-100 flex items-center justify-between gap-2 text-[12px] text-slate-500">
|
||||
<span class="truncate">{{ $locationText !== '' ? $locationText : 'Konum belirtilmedi' }}</span>
|
||||
<span class="shrink-0">{{ $listing->created_at?->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pt-2">
|
||||
{{ $listings->links() }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const countrySelect = document.querySelector('[data-listing-country]');
|
||||
const citySelect = document.querySelector('[data-listing-city]');
|
||||
const currentLocationButton = document.querySelector('[data-use-current-location]');
|
||||
const citiesTemplate = countrySelect?.dataset.citiesUrlTemplate ?? '';
|
||||
const locationStorageKey = 'oc2.header.location';
|
||||
|
||||
if (!countrySelect || !citySelect || citiesTemplate === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalize = (value) => (value ?? '')
|
||||
.toString()
|
||||
.toLocaleLowerCase('tr-TR')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim();
|
||||
|
||||
const setCityOptions = (cities, selectedCityName = '') => {
|
||||
citySelect.innerHTML = '<option value="">İlçe seçin</option>';
|
||||
cities.forEach((city) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = String(city.id ?? '');
|
||||
option.textContent = city.name ?? '';
|
||||
option.dataset.name = city.name ?? '';
|
||||
citySelect.appendChild(option);
|
||||
});
|
||||
citySelect.disabled = false;
|
||||
|
||||
if (selectedCityName) {
|
||||
const matched = Array.from(citySelect.options).find((option) => normalize(option.dataset.name) === normalize(selectedCityName));
|
||||
if (matched) {
|
||||
citySelect.value = matched.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCityOptions = async (url) => {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('city_fetch_failed');
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return Array.isArray(payload?.data) ? payload.data : [];
|
||||
};
|
||||
|
||||
const loadCities = async (countryId, selectedCityName = '') => {
|
||||
if (!countryId) {
|
||||
citySelect.innerHTML = '<option value="">Önce il seçin</option>';
|
||||
citySelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
citySelect.disabled = true;
|
||||
citySelect.innerHTML = '<option value="">İlçeler yükleniyor...</option>';
|
||||
|
||||
const primaryUrl = citiesTemplate.replace('__COUNTRY__', encodeURIComponent(String(countryId)));
|
||||
|
||||
try {
|
||||
let cities = [];
|
||||
|
||||
try {
|
||||
cities = await fetchCityOptions(primaryUrl);
|
||||
} catch (primaryError) {
|
||||
if (!/^https?:\/\//i.test(primaryUrl)) {
|
||||
throw primaryError;
|
||||
}
|
||||
|
||||
let fallbackUrl = null;
|
||||
|
||||
try {
|
||||
const parsed = new URL(primaryUrl);
|
||||
fallbackUrl = `${parsed.pathname}${parsed.search}`;
|
||||
} catch (urlError) {
|
||||
fallbackUrl = null;
|
||||
}
|
||||
|
||||
if (!fallbackUrl) {
|
||||
throw primaryError;
|
||||
}
|
||||
|
||||
cities = await fetchCityOptions(fallbackUrl);
|
||||
}
|
||||
|
||||
setCityOptions(cities, selectedCityName);
|
||||
} catch (error) {
|
||||
citySelect.innerHTML = '<option value="">İlçeler yüklenemedi</option>';
|
||||
citySelect.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
countrySelect.addEventListener('change', () => {
|
||||
citySelect.value = '';
|
||||
void loadCities(countrySelect.value);
|
||||
});
|
||||
|
||||
currentLocationButton?.addEventListener('click', async () => {
|
||||
try {
|
||||
const rawLocation = localStorage.getItem(locationStorageKey);
|
||||
if (!rawLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedLocation = JSON.parse(rawLocation);
|
||||
const countryName = parsedLocation?.countryName ?? '';
|
||||
const cityName = parsedLocation?.cityName ?? '';
|
||||
const countryId = parsedLocation?.countryId ? String(parsedLocation.countryId) : null;
|
||||
|
||||
const matchedCountryOption = Array.from(countrySelect.options).find((option) => {
|
||||
if (countryId && option.value === countryId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return normalize(option.textContent) === normalize(countryName);
|
||||
});
|
||||
|
||||
if (!matchedCountryOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
countrySelect.value = matchedCountryOption.value;
|
||||
await loadCities(matchedCountryOption.value, cityName);
|
||||
} catch (error) {
|
||||
// no-op
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@include('listing::partials.index-content')
|
||||
@endsection
|
||||
|
||||
454
Modules/Listing/resources/views/partials/index-content.blade.php
Normal file
454
Modules/Listing/resources/views/partials/index-content.blade.php
Normal file
@ -0,0 +1,454 @@
|
||||
@php
|
||||
$allListingsCount = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total();
|
||||
$resultListingsCount = isset($filteredListingsTotal) ? (int) $filteredListingsTotal : (int) $listings->total();
|
||||
$activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : '';
|
||||
$seoHeading = $activeCategoryName !== ''
|
||||
? $activeCategoryName.' Listings and Prices'
|
||||
: 'All Listings and Prices';
|
||||
$canSaveSearch = $search !== '' || ! is_null($categoryId);
|
||||
$normalizeQuery = static fn ($value): bool => ! is_null($value) && $value !== '';
|
||||
$baseCategoryQuery = array_filter([
|
||||
'search' => $search !== '' ? $search : null,
|
||||
'user' => $sellerUserId ?? null,
|
||||
'country' => $countryId,
|
||||
'city' => $cityId,
|
||||
'min_price' => $minPriceInput !== '' ? $minPriceInput : null,
|
||||
'max_price' => $maxPriceInput !== '' ? $maxPriceInput : null,
|
||||
'date_filter' => $dateFilter !== 'all' ? $dateFilter : null,
|
||||
'sort' => $sort !== 'smart' ? $sort : null,
|
||||
], $normalizeQuery);
|
||||
$clearFiltersQuery = array_filter([
|
||||
'search' => $search !== '' ? $search : null,
|
||||
'user' => $sellerUserId ?? null,
|
||||
], $normalizeQuery);
|
||||
@endphp
|
||||
|
||||
<div class="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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 max-h-[330px] overflow-y-auto pr-1">
|
||||
@php
|
||||
$allCategoriesLink = route('listings.index', $baseCategoryQuery);
|
||||
@endphp
|
||||
<a href="{{ $allCategoriesLink }}" class="flex items-center justify-between rounded-lg px-2.5 py-2 text-sm font-semibold {{ is_null($categoryId) ? 'bg-slate-900 text-white' : 'text-slate-700 hover:bg-slate-100' }}">
|
||||
<span>All Listings</span>
|
||||
<span>{{ number_format($allListingsCount) }}</span>
|
||||
</a>
|
||||
|
||||
@foreach($categories as $category)
|
||||
@php
|
||||
$categoryCount = (int) $category->active_listing_total;
|
||||
$isSelectedParent = (int) $categoryId === (int) $category->id;
|
||||
$categoryUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $category->id,
|
||||
]), $normalizeQuery));
|
||||
@endphp
|
||||
<a href="{{ $categoryUrl }}" class="flex items-center justify-between rounded-lg px-2.5 py-2 text-sm font-semibold {{ $isSelectedParent ? 'bg-slate-900 text-white' : 'text-slate-700 hover:bg-slate-100' }}">
|
||||
<span>{{ $category->name }}</span>
|
||||
<span>{{ number_format($categoryCount) }}</span>
|
||||
</a>
|
||||
|
||||
@foreach($category->children as $childCategory)
|
||||
@php
|
||||
$isSelectedChild = (int) $categoryId === (int) $childCategory->id;
|
||||
$childUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $childCategory->id,
|
||||
]), $normalizeQuery));
|
||||
@endphp
|
||||
<a href="{{ $childUrl }}" class="ml-2 flex items-center justify-between rounded-lg px-2 py-1.5 text-[13px] font-medium {{ $isSelectedChild ? 'bg-rose-50 text-rose-600' : 'text-slate-600 hover:bg-slate-100' }}">
|
||||
<span>{{ $childCategory->name }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listing_total) }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form method="GET" action="{{ route('listings.index') }}" class="listing-filter-card p-4 space-y-5">
|
||||
@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
|
||||
<input type="hidden" name="sort" value="{{ $sort }}">
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">Location</h3>
|
||||
<div class="space-y-2.5">
|
||||
@php
|
||||
$citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities')
|
||||
? route('locations.cities', ['country' => '__COUNTRY__'], false)
|
||||
: '';
|
||||
@endphp
|
||||
<select
|
||||
name="country"
|
||||
data-listing-country
|
||||
data-cities-url-template="{{ $citiesRouteTemplate }}"
|
||||
class="w-full h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200"
|
||||
>
|
||||
<option value="">Select country</option>
|
||||
@foreach($countries as $country)
|
||||
<option value="{{ $country->id }}" @selected((int) $countryId === (int) $country->id)>
|
||||
{{ $country->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<select name="city" data-listing-city class="w-full h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200" @disabled(!$countryId)>
|
||||
<option value="">{{ $countryId ? 'Select city' : 'Select country first' }}</option>
|
||||
@foreach($cities as $city)
|
||||
<option value="{{ $city->id }}" @selected((int) $cityId === (int) $city->id)>
|
||||
{{ $city->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<button type="button" data-use-current-location class="w-full h-10 rounded-lg border border-slate-300 bg-white text-sm font-semibold text-slate-700 hover:bg-slate-50 transition">
|
||||
Use current location
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">Price</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input type="number" name="min_price" value="{{ $minPriceInput }}" min="0" step="1" placeholder="Min" class="h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200">
|
||||
<input type="number" name="max_price" value="{{ $maxPriceInput }}" min="0" step="1" placeholder="Max" class="h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">Posted date</h3>
|
||||
<div class="space-y-2 text-sm text-slate-700">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="all" class="accent-rose-500" @checked($dateFilter === 'all')>
|
||||
<span>All</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="today" class="accent-rose-500" @checked($dateFilter === 'today')>
|
||||
<span>Today</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="week" class="accent-rose-500" @checked($dateFilter === 'week')>
|
||||
<span>Last 7 days</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="month" class="accent-rose-500" @checked($dateFilter === 'month')>
|
||||
<span>Last 30 days</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('listings.index', $clearFiltersQuery) }}" class="flex-1 h-10 inline-flex items-center justify-center rounded-full border border-rose-300 text-rose-500 text-sm font-semibold hover:bg-rose-50 transition">
|
||||
Clear
|
||||
</a>
|
||||
<button type="submit" class="flex-1 h-10 rounded-full bg-rose-500 text-white text-sm font-semibold hover:bg-rose-600 transition">
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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">
|
||||
<p class="text-sm text-slate-700 mr-auto">
|
||||
<strong>{{ number_format($resultListingsCount) }}</strong>
|
||||
{{ $activeCategoryName !== '' ? ' listings found in '.$activeCategoryName : ' listings found' }}
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.searches.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
<input type="hidden" name="category_id" value="{{ $categoryId }}">
|
||||
<button type="submit" class="h-10 px-4 rounded-full border text-sm font-semibold transition {{ $isCurrentSearchSaved ? 'bg-emerald-100 border-emerald-200 text-emerald-700 cursor-default' : ($canSaveSearch ? 'bg-rose-50 border-rose-200 text-rose-600 hover:bg-rose-100' : 'bg-slate-100 border-slate-200 text-slate-400 cursor-not-allowed') }}" @disabled($isCurrentSearchSaved || ! $canSaveSearch)>
|
||||
{{ $isCurrentSearchSaved ? 'Search saved' : 'Save search' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="h-10 px-4 inline-flex items-center rounded-full border border-slate-300 text-sm font-semibold text-slate-600 hover:bg-slate-50 transition">
|
||||
Save search
|
||||
</a>
|
||||
@endauth
|
||||
|
||||
<form method="GET" action="{{ route('listings.index') }}">
|
||||
@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="h-10 px-4 rounded-full border border-slate-300 bg-white inline-flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||
<span>Sort by</span>
|
||||
<select name="sort" class="bg-transparent text-sm font-semibold focus:outline-none" 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: low to high</option>
|
||||
<option value="price_desc" @selected($sort === 'price_desc')>Price: high to low</option>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($listings->isEmpty())
|
||||
<div class="listing-filter-card py-14 text-center text-slate-500">
|
||||
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">
|
||||
@foreach($listings as $listing)
|
||||
@php
|
||||
$listingImage = $listing->getFirstMediaUrl('listing-images');
|
||||
$isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
|
||||
$priceValue = ! is_null($listing->price) ? (float) $listing->price : null;
|
||||
$locationParts = array_filter([
|
||||
trim((string) ($listing->city ?? '')),
|
||||
trim((string) ($listing->country ?? '')),
|
||||
], fn ($value) => $value !== '');
|
||||
$locationText = implode(', ', $locationParts);
|
||||
@endphp
|
||||
<article class="listing-card">
|
||||
<div class="relative h-52 bg-slate-200">
|
||||
@if($listingImage)
|
||||
<a href="{{ route('listings.show', $listing) }}" class="block w-full h-full">
|
||||
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover">
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('listings.show', $listing) }}" class="w-full h-full grid place-items-center text-slate-400">
|
||||
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 16l4.6-4.6a2 2 0 012.8 0L16 16m-2-2 1.6-1.6a2 2 0 012.8 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($listing->is_featured)
|
||||
<span class="absolute top-2 left-2 inline-flex items-center rounded-full bg-yellow-300 text-slate-900 text-[11px] font-bold px-2.5 py-1">
|
||||
Featured
|
||||
</span>
|
||||
@endif
|
||||
|
||||
<div class="absolute top-2 right-2">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="w-8 h-8 rounded-full grid place-items-center transition {{ $isFavorited ? 'bg-rose-500 text-white' : 'bg-white text-slate-500 hover:text-rose-500' }}" aria-label="Save listing">
|
||||
♥
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="w-8 h-8 rounded-full bg-white text-slate-500 hover:text-rose-500 grid place-items-center transition" aria-label="Sign in">
|
||||
♥
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
@if(!is_null($priceValue) && $priceValue > 0)
|
||||
{{ number_format($priceValue, 0) }} {{ $listing->currency }}
|
||||
@else
|
||||
Free
|
||||
@endif
|
||||
</p>
|
||||
<h3 class="listing-title mt-2 text-sm font-semibold text-slate-900">
|
||||
{{ $listing->title }}
|
||||
</h3>
|
||||
</a>
|
||||
|
||||
<p class="text-xs text-slate-500 mt-2">
|
||||
{{ $listing->category?->name ?: 'No category' }}
|
||||
</p>
|
||||
|
||||
<div class="mt-3 pt-2 border-t border-slate-100 flex items-center justify-between gap-2 text-[12px] text-slate-500">
|
||||
<span class="truncate">{{ $locationText !== '' ? $locationText : 'Location not specified' }}</span>
|
||||
<span class="shrink-0">{{ $listing->created_at?->format('M j, Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pt-2">
|
||||
{{ $listings->links() }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const countrySelect = document.querySelector('[data-listing-country]');
|
||||
const citySelect = document.querySelector('[data-listing-city]');
|
||||
const currentLocationButton = document.querySelector('[data-use-current-location]');
|
||||
const citiesTemplate = countrySelect?.dataset.citiesUrlTemplate ?? '';
|
||||
const locationStorageKey = 'oc2.header.location';
|
||||
|
||||
if (!countrySelect || !citySelect || citiesTemplate === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalize = (value) => (value ?? '')
|
||||
.toString()
|
||||
.toLocaleLowerCase('tr-TR')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim();
|
||||
|
||||
const setCityOptions = (cities, selectedCityName = '') => {
|
||||
citySelect.innerHTML = '<option value="">Select city</option>';
|
||||
cities.forEach((city) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = String(city.id ?? '');
|
||||
option.textContent = city.name ?? '';
|
||||
option.dataset.name = city.name ?? '';
|
||||
citySelect.appendChild(option);
|
||||
});
|
||||
citySelect.disabled = false;
|
||||
|
||||
if (selectedCityName) {
|
||||
const matched = Array.from(citySelect.options).find((option) => normalize(option.dataset.name) === normalize(selectedCityName));
|
||||
if (matched) {
|
||||
citySelect.value = matched.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCityOptions = async (url) => {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('city_fetch_failed');
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return Array.isArray(payload?.data) ? payload.data : [];
|
||||
};
|
||||
|
||||
const loadCities = async (countryId, selectedCityName = '') => {
|
||||
if (!countryId) {
|
||||
citySelect.innerHTML = '<option value="">Select country first</option>';
|
||||
citySelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
citySelect.disabled = true;
|
||||
citySelect.innerHTML = '<option value="">Loading cities...</option>';
|
||||
|
||||
const primaryUrl = citiesTemplate.replace('__COUNTRY__', encodeURIComponent(String(countryId)));
|
||||
|
||||
try {
|
||||
let cities = [];
|
||||
|
||||
try {
|
||||
cities = await fetchCityOptions(primaryUrl);
|
||||
} catch (primaryError) {
|
||||
if (!/^https?:\/\//i.test(primaryUrl)) {
|
||||
throw primaryError;
|
||||
}
|
||||
|
||||
let fallbackUrl = null;
|
||||
|
||||
try {
|
||||
const parsed = new URL(primaryUrl);
|
||||
fallbackUrl = `${parsed.pathname}${parsed.search}`;
|
||||
} catch (urlError) {
|
||||
fallbackUrl = null;
|
||||
}
|
||||
|
||||
if (!fallbackUrl) {
|
||||
throw primaryError;
|
||||
}
|
||||
|
||||
cities = await fetchCityOptions(fallbackUrl);
|
||||
}
|
||||
|
||||
setCityOptions(cities, selectedCityName);
|
||||
} catch (error) {
|
||||
citySelect.innerHTML = '<option value="">Cities could not be loaded</option>';
|
||||
citySelect.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
countrySelect.addEventListener('change', () => {
|
||||
citySelect.value = '';
|
||||
void loadCities(countrySelect.value);
|
||||
});
|
||||
|
||||
currentLocationButton?.addEventListener('click', async () => {
|
||||
try {
|
||||
const rawLocation = localStorage.getItem(locationStorageKey);
|
||||
if (!rawLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedLocation = JSON.parse(rawLocation);
|
||||
const countryName = parsedLocation?.countryName ?? '';
|
||||
const cityName = parsedLocation?.cityName ?? '';
|
||||
const countryId = parsedLocation?.countryId ? String(parsedLocation.countryId) : null;
|
||||
|
||||
const matchedCountryOption = Array.from(countrySelect.options).find((option) => {
|
||||
if (countryId && option.value === countryId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return normalize(option.textContent) === normalize(countryName);
|
||||
});
|
||||
|
||||
if (!matchedCountryOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
countrySelect.value = matchedCountryOption.value;
|
||||
await loadCities(matchedCountryOption.value, cityName);
|
||||
} catch (error) {
|
||||
// no-op
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@ -42,32 +42,32 @@
|
||||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold transition {{ $isListingFavorited ? 'bg-rose-100 text-rose-700' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||
{{ $isListingFavorited ? '♥ Favorilerde' : '♡ Favoriye Ekle' }}
|
||||
{{ $isListingFavorited ? '♥ Saved' : '♡ Save listing' }}
|
||||
</button>
|
||||
</form>
|
||||
@if($listing->user && (int) $listing->user->id !== (int) auth()->id())
|
||||
<form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold transition {{ $isSellerFavorited ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||
{{ $isSellerFavorited ? 'Satıcı Favorilerde' : 'Satıcıyı Takip Et' }}
|
||||
{{ $isSellerFavorited ? 'Seller saved' : 'Save seller' }}
|
||||
</button>
|
||||
</form>
|
||||
@if($existingConversationId)
|
||||
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-rose-100 text-rose-700 hover:bg-rose-200 transition">
|
||||
Sohbete Git
|
||||
Open chat
|
||||
</a>
|
||||
@else
|
||||
<form method="POST" action="{{ route('conversations.start', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-rose-500 text-white hover:bg-rose-600 transition">
|
||||
Satıcıya Mesaj Gönder
|
||||
Message seller
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-slate-100 text-slate-700 hover:bg-slate-200 transition">
|
||||
Giriş yap ve favorile
|
||||
Log in to save
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
@ -79,7 +79,7 @@
|
||||
</div>
|
||||
@if(($presentableCustomFields ?? []) !== [])
|
||||
<div class="mt-6 border-t pt-4">
|
||||
<h2 class="font-semibold text-lg mb-3">İlan Özellikleri</h2>
|
||||
<h2 class="font-semibold text-lg mb-3">Listing details</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
@foreach($presentableCustomFields as $field)
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
|
||||
|
||||
@ -1,450 +1,7 @@
|
||||
@extends('app::layouts.app')
|
||||
|
||||
@section('title', trim((string) ($selectedCategory?->name ?? '')) !== '' ? trim((string) $selectedCategory->name).' Listings and Prices' : 'All Listings and Prices')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$allListingsCount = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total();
|
||||
$resultListingsCount = isset($filteredListingsTotal) ? (int) $filteredListingsTotal : (int) $listings->total();
|
||||
$activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : '';
|
||||
$pageTitle = $activeCategoryName !== ''
|
||||
? 'İkinci El '.$activeCategoryName.' İlanları ve Fiyatları'
|
||||
: 'İkinci El Araba İlanları ve Fiyatları';
|
||||
$canSaveSearch = $search !== '' || ! is_null($categoryId);
|
||||
$normalizeQuery = static fn ($value): bool => ! is_null($value) && $value !== '';
|
||||
$baseCategoryQuery = array_filter([
|
||||
'search' => $search !== '' ? $search : null,
|
||||
'country' => $countryId,
|
||||
'city' => $cityId,
|
||||
'min_price' => $minPriceInput !== '' ? $minPriceInput : null,
|
||||
'max_price' => $maxPriceInput !== '' ? $maxPriceInput : null,
|
||||
'date_filter' => $dateFilter !== 'all' ? $dateFilter : null,
|
||||
'sort' => $sort !== 'smart' ? $sort : null,
|
||||
], $normalizeQuery);
|
||||
$clearFiltersQuery = array_filter([
|
||||
'search' => $search !== '' ? $search : null,
|
||||
], $normalizeQuery);
|
||||
@endphp
|
||||
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
|
||||
<h1 class="text-3xl md:text-4xl leading-tight font-bold text-slate-900 mb-6">{{ $pageTitle }}</h1>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[260px,1fr] gap-4 lg:gap-5">
|
||||
<aside class="space-y-4">
|
||||
<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">Kategoriler</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 max-h-[330px] overflow-y-auto pr-1">
|
||||
@php
|
||||
$allCategoriesLink = route('listings.index', $baseCategoryQuery);
|
||||
@endphp
|
||||
<a href="{{ $allCategoriesLink }}" class="flex items-center justify-between rounded-lg px-2.5 py-2 text-sm font-semibold {{ is_null($categoryId) ? 'bg-slate-900 text-white' : 'text-slate-700 hover:bg-slate-100' }}">
|
||||
<span>Tüm İlanlar</span>
|
||||
<span>{{ number_format($allListingsCount, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
|
||||
@foreach($categories as $category)
|
||||
@php
|
||||
$categoryCount = (int) $category->active_listing_total;
|
||||
$isSelectedParent = (int) $categoryId === (int) $category->id;
|
||||
$categoryUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $category->id,
|
||||
]), $normalizeQuery));
|
||||
@endphp
|
||||
<a href="{{ $categoryUrl }}" class="flex items-center justify-between rounded-lg px-2.5 py-2 text-sm font-semibold {{ $isSelectedParent ? 'bg-slate-900 text-white' : 'text-slate-700 hover:bg-slate-100' }}">
|
||||
<span>{{ $category->name }}</span>
|
||||
<span>{{ number_format($categoryCount, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
|
||||
@foreach($category->children as $childCategory)
|
||||
@php
|
||||
$isSelectedChild = (int) $categoryId === (int) $childCategory->id;
|
||||
$childUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $childCategory->id,
|
||||
]), $normalizeQuery));
|
||||
@endphp
|
||||
<a href="{{ $childUrl }}" class="ml-2 flex items-center justify-between rounded-lg px-2 py-1.5 text-[13px] font-medium {{ $isSelectedChild ? 'bg-rose-50 text-rose-600' : 'text-slate-600 hover:bg-slate-100' }}">
|
||||
<span>{{ $childCategory->name }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listing_total, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form method="GET" action="{{ route('listings.index') }}" class="listing-filter-card p-4 space-y-5">
|
||||
@if($search !== '')
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
@endif
|
||||
@if($categoryId)
|
||||
<input type="hidden" name="category" value="{{ $categoryId }}">
|
||||
@endif
|
||||
<input type="hidden" name="sort" value="{{ $sort }}">
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">Konum</h3>
|
||||
<div class="space-y-2.5">
|
||||
@php
|
||||
$citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities')
|
||||
? route('locations.cities', ['country' => '__COUNTRY__'], false)
|
||||
: '';
|
||||
@endphp
|
||||
<select
|
||||
name="country"
|
||||
data-listing-country
|
||||
data-cities-url-template="{{ $citiesRouteTemplate }}"
|
||||
class="w-full h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200"
|
||||
>
|
||||
<option value="">İl seçin</option>
|
||||
@foreach($countries as $country)
|
||||
<option value="{{ $country->id }}" @selected((int) $countryId === (int) $country->id)>
|
||||
{{ $country->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<select name="city" data-listing-city class="w-full h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200" @disabled(!$countryId)>
|
||||
<option value="">{{ $countryId ? 'İlçe seçin' : 'Önce il seçin' }}</option>
|
||||
@foreach($cities as $city)
|
||||
<option value="{{ $city->id }}" @selected((int) $cityId === (int) $city->id)>
|
||||
{{ $city->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<button type="button" data-use-current-location class="w-full h-10 rounded-lg border border-slate-300 bg-white text-sm font-semibold text-slate-700 hover:bg-slate-50 transition">
|
||||
Mevcut konumu kullan
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">Fiyat</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input type="number" name="min_price" value="{{ $minPriceInput }}" min="0" step="1" placeholder="Min" class="h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200">
|
||||
<input type="number" name="max_price" value="{{ $maxPriceInput }}" min="0" step="1" placeholder="Maks" class="h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">İlan Tarihi</h3>
|
||||
<div class="space-y-2 text-sm text-slate-700">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="all" class="accent-rose-500" @checked($dateFilter === 'all')>
|
||||
<span>Tümü</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="today" class="accent-rose-500" @checked($dateFilter === 'today')>
|
||||
<span>Bugün</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="week" class="accent-rose-500" @checked($dateFilter === 'week')>
|
||||
<span>Son 7 Gün</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="month" class="accent-rose-500" @checked($dateFilter === 'month')>
|
||||
<span>Son 30 Gün</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('listings.index', $clearFiltersQuery) }}" class="flex-1 h-10 inline-flex items-center justify-center rounded-full border border-rose-300 text-rose-500 text-sm font-semibold hover:bg-rose-50 transition">
|
||||
Temizle
|
||||
</a>
|
||||
<button type="submit" class="flex-1 h-10 rounded-full bg-rose-500 text-white text-sm font-semibold hover:bg-rose-600 transition">
|
||||
Uygula
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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">
|
||||
<p class="text-sm text-slate-700 mr-auto">
|
||||
{{ $activeCategoryName !== '' ? 'İkinci El '.$activeCategoryName.' kategorisinde' : 'İkinci El Araba kategorisinde' }}
|
||||
<strong>{{ number_format($resultListingsCount, 0, ',', '.') }}</strong>
|
||||
ilan bulundu
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.searches.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
<input type="hidden" name="category_id" value="{{ $categoryId }}">
|
||||
<button type="submit" class="h-10 px-4 rounded-full border text-sm font-semibold transition {{ $isCurrentSearchSaved ? 'bg-emerald-100 border-emerald-200 text-emerald-700 cursor-default' : ($canSaveSearch ? 'bg-rose-50 border-rose-200 text-rose-600 hover:bg-rose-100' : 'bg-slate-100 border-slate-200 text-slate-400 cursor-not-allowed') }}" @disabled($isCurrentSearchSaved || ! $canSaveSearch)>
|
||||
{{ $isCurrentSearchSaved ? 'Arama Kaydedildi' : 'Arama Kaydet' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="h-10 px-4 inline-flex items-center rounded-full border border-slate-300 text-sm font-semibold text-slate-600 hover:bg-slate-50 transition">
|
||||
Arama Kaydet
|
||||
</a>
|
||||
@endauth
|
||||
|
||||
<form method="GET" action="{{ route('listings.index') }}">
|
||||
@if($search !== '')
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
@endif
|
||||
@if($categoryId)
|
||||
<input type="hidden" name="category" value="{{ $categoryId }}">
|
||||
@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="h-10 px-4 rounded-full border border-slate-300 bg-white inline-flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||
<span>Akıllı Sıralama</span>
|
||||
<select name="sort" class="bg-transparent text-sm font-semibold focus:outline-none" onchange="this.form.submit()">
|
||||
<option value="smart" @selected($sort === 'smart')>Önerilen</option>
|
||||
<option value="newest" @selected($sort === 'newest')>En Yeni</option>
|
||||
<option value="oldest" @selected($sort === 'oldest')>En Eski</option>
|
||||
<option value="price_asc" @selected($sort === 'price_asc')>Fiyat Artan</option>
|
||||
<option value="price_desc" @selected($sort === 'price_desc')>Fiyat Azalan</option>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($listings->isEmpty())
|
||||
<div class="listing-filter-card py-14 text-center text-slate-500">
|
||||
Bu filtreye uygun ilan bulunamadı.
|
||||
</div>
|
||||
@else
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3.5">
|
||||
@foreach($listings as $listing)
|
||||
@php
|
||||
$listingImage = $listing->getFirstMediaUrl('listing-images');
|
||||
$isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
|
||||
$priceValue = ! is_null($listing->price) ? (float) $listing->price : null;
|
||||
$locationParts = array_filter([
|
||||
trim((string) ($listing->city ?? '')),
|
||||
trim((string) ($listing->country ?? '')),
|
||||
], fn ($value) => $value !== '');
|
||||
$locationText = implode(', ', $locationParts);
|
||||
@endphp
|
||||
<article class="listing-card">
|
||||
<div class="relative h-52 bg-slate-200">
|
||||
@if($listingImage)
|
||||
<a href="{{ route('listings.show', $listing) }}" class="block w-full h-full">
|
||||
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover">
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('listings.show', $listing) }}" class="w-full h-full grid place-items-center text-slate-400">
|
||||
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 16l4.6-4.6a2 2 0 012.8 0L16 16m-2-2 1.6-1.6a2 2 0 012.8 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($listing->is_featured)
|
||||
<span class="absolute top-2 left-2 inline-flex items-center rounded-full bg-yellow-300 text-slate-900 text-[11px] font-bold px-2.5 py-1">
|
||||
Öne Çıkan
|
||||
</span>
|
||||
@endif
|
||||
|
||||
<div class="absolute top-2 right-2">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="w-8 h-8 rounded-full grid place-items-center transition {{ $isFavorited ? 'bg-rose-500 text-white' : 'bg-white text-slate-500 hover:text-rose-500' }}" aria-label="Favoriye ekle">
|
||||
♥
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="w-8 h-8 rounded-full bg-white text-slate-500 hover:text-rose-500 grid place-items-center transition" aria-label="Giriş yap">
|
||||
♥
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
@if(!is_null($priceValue) && $priceValue > 0)
|
||||
{{ number_format($priceValue, 0, ',', '.') }} {{ $listing->currency }}
|
||||
@else
|
||||
Ücretsiz
|
||||
@endif
|
||||
</p>
|
||||
<h3 class="listing-title mt-2 text-sm font-semibold text-slate-900">
|
||||
{{ $listing->title }}
|
||||
</h3>
|
||||
</a>
|
||||
|
||||
<p class="text-xs text-slate-500 mt-2">
|
||||
{{ $listing->category?->name ?: 'Kategori yok' }}
|
||||
</p>
|
||||
|
||||
<div class="mt-3 pt-2 border-t border-slate-100 flex items-center justify-between gap-2 text-[12px] text-slate-500">
|
||||
<span class="truncate">{{ $locationText !== '' ? $locationText : 'Konum belirtilmedi' }}</span>
|
||||
<span class="shrink-0">{{ $listing->created_at?->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pt-2">
|
||||
{{ $listings->links() }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const countrySelect = document.querySelector('[data-listing-country]');
|
||||
const citySelect = document.querySelector('[data-listing-city]');
|
||||
const currentLocationButton = document.querySelector('[data-use-current-location]');
|
||||
const citiesTemplate = countrySelect?.dataset.citiesUrlTemplate ?? '';
|
||||
const locationStorageKey = 'oc2.header.location';
|
||||
|
||||
if (!countrySelect || !citySelect || citiesTemplate === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalize = (value) => (value ?? '')
|
||||
.toString()
|
||||
.toLocaleLowerCase('tr-TR')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim();
|
||||
|
||||
const setCityOptions = (cities, selectedCityName = '') => {
|
||||
citySelect.innerHTML = '<option value="">İlçe seçin</option>';
|
||||
cities.forEach((city) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = String(city.id ?? '');
|
||||
option.textContent = city.name ?? '';
|
||||
option.dataset.name = city.name ?? '';
|
||||
citySelect.appendChild(option);
|
||||
});
|
||||
citySelect.disabled = false;
|
||||
|
||||
if (selectedCityName) {
|
||||
const matched = Array.from(citySelect.options).find((option) => normalize(option.dataset.name) === normalize(selectedCityName));
|
||||
if (matched) {
|
||||
citySelect.value = matched.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCityOptions = async (url) => {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('city_fetch_failed');
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return Array.isArray(payload?.data) ? payload.data : [];
|
||||
};
|
||||
|
||||
const loadCities = async (countryId, selectedCityName = '') => {
|
||||
if (!countryId) {
|
||||
citySelect.innerHTML = '<option value="">Önce il seçin</option>';
|
||||
citySelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
citySelect.disabled = true;
|
||||
citySelect.innerHTML = '<option value="">İlçeler yükleniyor...</option>';
|
||||
|
||||
const primaryUrl = citiesTemplate.replace('__COUNTRY__', encodeURIComponent(String(countryId)));
|
||||
|
||||
try {
|
||||
let cities = [];
|
||||
|
||||
try {
|
||||
cities = await fetchCityOptions(primaryUrl);
|
||||
} catch (primaryError) {
|
||||
if (!/^https?:\/\//i.test(primaryUrl)) {
|
||||
throw primaryError;
|
||||
}
|
||||
|
||||
let fallbackUrl = null;
|
||||
|
||||
try {
|
||||
const parsed = new URL(primaryUrl);
|
||||
fallbackUrl = `${parsed.pathname}${parsed.search}`;
|
||||
} catch (urlError) {
|
||||
fallbackUrl = null;
|
||||
}
|
||||
|
||||
if (!fallbackUrl) {
|
||||
throw primaryError;
|
||||
}
|
||||
|
||||
cities = await fetchCityOptions(fallbackUrl);
|
||||
}
|
||||
|
||||
setCityOptions(cities, selectedCityName);
|
||||
} catch (error) {
|
||||
citySelect.innerHTML = '<option value="">İlçeler yüklenemedi</option>';
|
||||
citySelect.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
countrySelect.addEventListener('change', () => {
|
||||
citySelect.value = '';
|
||||
void loadCities(countrySelect.value);
|
||||
});
|
||||
|
||||
currentLocationButton?.addEventListener('click', async () => {
|
||||
try {
|
||||
const rawLocation = localStorage.getItem(locationStorageKey);
|
||||
if (!rawLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedLocation = JSON.parse(rawLocation);
|
||||
const countryName = parsedLocation?.countryName ?? '';
|
||||
const cityName = parsedLocation?.cityName ?? '';
|
||||
const countryId = parsedLocation?.countryId ? String(parsedLocation.countryId) : null;
|
||||
|
||||
const matchedCountryOption = Array.from(countrySelect.options).find((option) => {
|
||||
if (countryId && option.value === countryId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return normalize(option.textContent) === normalize(countryName);
|
||||
});
|
||||
|
||||
if (!matchedCountryOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
countrySelect.value = matchedCountryOption.value;
|
||||
await loadCities(matchedCountryOption.value, cityName);
|
||||
} catch (error) {
|
||||
// no-op
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@include('listing::partials.index-content')
|
||||
@endsection
|
||||
|
||||
@ -42,32 +42,32 @@
|
||||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold transition {{ $isListingFavorited ? 'bg-rose-100 text-rose-700' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||
{{ $isListingFavorited ? '♥ Favorilerde' : '♡ Favoriye Ekle' }}
|
||||
{{ $isListingFavorited ? '♥ Saved' : '♡ Save listing' }}
|
||||
</button>
|
||||
</form>
|
||||
@if($listing->user && (int) $listing->user->id !== (int) auth()->id())
|
||||
<form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold transition {{ $isSellerFavorited ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||
{{ $isSellerFavorited ? 'Satıcı Favorilerde' : 'Satıcıyı Takip Et' }}
|
||||
{{ $isSellerFavorited ? 'Seller saved' : 'Save seller' }}
|
||||
</button>
|
||||
</form>
|
||||
@if($existingConversationId)
|
||||
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-rose-100 text-rose-700 hover:bg-rose-200 transition">
|
||||
Sohbete Git
|
||||
Open chat
|
||||
</a>
|
||||
@else
|
||||
<form method="POST" action="{{ route('conversations.start', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-rose-500 text-white hover:bg-rose-600 transition">
|
||||
Satıcıya Mesaj Gönder
|
||||
Message seller
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-slate-100 text-slate-700 hover:bg-slate-200 transition">
|
||||
Giriş yap ve favorile
|
||||
Log in to save
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
@ -92,7 +92,7 @@
|
||||
@endif
|
||||
@if(($presentableCustomFields ?? []) !== [])
|
||||
<div class="mt-6 border-t pt-4">
|
||||
<h2 class="font-semibold text-lg mb-3">İlan Özellikleri</h2>
|
||||
<h2 class="font-semibold text-lg mb-3">Listing details</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
@foreach($presentableCustomFields as $field)
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
|
||||
|
||||
@ -1,450 +1,7 @@
|
||||
@extends('app::layouts.app')
|
||||
|
||||
@section('title', trim((string) ($selectedCategory?->name ?? '')) !== '' ? trim((string) $selectedCategory->name).' Listings and Prices' : 'All Listings and Prices')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$allListingsCount = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total();
|
||||
$resultListingsCount = isset($filteredListingsTotal) ? (int) $filteredListingsTotal : (int) $listings->total();
|
||||
$activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : '';
|
||||
$pageTitle = $activeCategoryName !== ''
|
||||
? 'İkinci El '.$activeCategoryName.' İlanları ve Fiyatları'
|
||||
: 'İkinci El Araba İlanları ve Fiyatları';
|
||||
$canSaveSearch = $search !== '' || ! is_null($categoryId);
|
||||
$normalizeQuery = static fn ($value): bool => ! is_null($value) && $value !== '';
|
||||
$baseCategoryQuery = array_filter([
|
||||
'search' => $search !== '' ? $search : null,
|
||||
'country' => $countryId,
|
||||
'city' => $cityId,
|
||||
'min_price' => $minPriceInput !== '' ? $minPriceInput : null,
|
||||
'max_price' => $maxPriceInput !== '' ? $maxPriceInput : null,
|
||||
'date_filter' => $dateFilter !== 'all' ? $dateFilter : null,
|
||||
'sort' => $sort !== 'smart' ? $sort : null,
|
||||
], $normalizeQuery);
|
||||
$clearFiltersQuery = array_filter([
|
||||
'search' => $search !== '' ? $search : null,
|
||||
], $normalizeQuery);
|
||||
@endphp
|
||||
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
|
||||
<h1 class="text-3xl md:text-4xl leading-tight font-bold text-slate-900 mb-6">{{ $pageTitle }}</h1>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-[260px,1fr] gap-4 lg:gap-5">
|
||||
<aside class="space-y-4">
|
||||
<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">Kategoriler</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 max-h-[330px] overflow-y-auto pr-1">
|
||||
@php
|
||||
$allCategoriesLink = route('listings.index', $baseCategoryQuery);
|
||||
@endphp
|
||||
<a href="{{ $allCategoriesLink }}" class="flex items-center justify-between rounded-lg px-2.5 py-2 text-sm font-semibold {{ is_null($categoryId) ? 'bg-slate-900 text-white' : 'text-slate-700 hover:bg-slate-100' }}">
|
||||
<span>Tüm İlanlar</span>
|
||||
<span>{{ number_format($allListingsCount, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
|
||||
@foreach($categories as $category)
|
||||
@php
|
||||
$categoryCount = (int) $category->active_listing_total;
|
||||
$isSelectedParent = (int) $categoryId === (int) $category->id;
|
||||
$categoryUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $category->id,
|
||||
]), $normalizeQuery));
|
||||
@endphp
|
||||
<a href="{{ $categoryUrl }}" class="flex items-center justify-between rounded-lg px-2.5 py-2 text-sm font-semibold {{ $isSelectedParent ? 'bg-slate-900 text-white' : 'text-slate-700 hover:bg-slate-100' }}">
|
||||
<span>{{ $category->name }}</span>
|
||||
<span>{{ number_format($categoryCount, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
|
||||
@foreach($category->children as $childCategory)
|
||||
@php
|
||||
$isSelectedChild = (int) $categoryId === (int) $childCategory->id;
|
||||
$childUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $childCategory->id,
|
||||
]), $normalizeQuery));
|
||||
@endphp
|
||||
<a href="{{ $childUrl }}" class="ml-2 flex items-center justify-between rounded-lg px-2 py-1.5 text-[13px] font-medium {{ $isSelectedChild ? 'bg-rose-50 text-rose-600' : 'text-slate-600 hover:bg-slate-100' }}">
|
||||
<span>{{ $childCategory->name }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listing_total, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form method="GET" action="{{ route('listings.index') }}" class="listing-filter-card p-4 space-y-5">
|
||||
@if($search !== '')
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
@endif
|
||||
@if($categoryId)
|
||||
<input type="hidden" name="category" value="{{ $categoryId }}">
|
||||
@endif
|
||||
<input type="hidden" name="sort" value="{{ $sort }}">
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">Konum</h3>
|
||||
<div class="space-y-2.5">
|
||||
@php
|
||||
$citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities')
|
||||
? route('locations.cities', ['country' => '__COUNTRY__'], false)
|
||||
: '';
|
||||
@endphp
|
||||
<select
|
||||
name="country"
|
||||
data-listing-country
|
||||
data-cities-url-template="{{ $citiesRouteTemplate }}"
|
||||
class="w-full h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200"
|
||||
>
|
||||
<option value="">İl seçin</option>
|
||||
@foreach($countries as $country)
|
||||
<option value="{{ $country->id }}" @selected((int) $countryId === (int) $country->id)>
|
||||
{{ $country->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<select name="city" data-listing-city class="w-full h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200" @disabled(!$countryId)>
|
||||
<option value="">{{ $countryId ? 'İlçe seçin' : 'Önce il seçin' }}</option>
|
||||
@foreach($cities as $city)
|
||||
<option value="{{ $city->id }}" @selected((int) $cityId === (int) $city->id)>
|
||||
{{ $city->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<button type="button" data-use-current-location class="w-full h-10 rounded-lg border border-slate-300 bg-white text-sm font-semibold text-slate-700 hover:bg-slate-50 transition">
|
||||
Mevcut konumu kullan
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">Fiyat</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input type="number" name="min_price" value="{{ $minPriceInput }}" min="0" step="1" placeholder="Min" class="h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200">
|
||||
<input type="number" name="max_price" value="{{ $maxPriceInput }}" min="0" step="1" placeholder="Maks" class="h-10 rounded-lg border border-slate-300 bg-slate-50 px-3 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-rose-200">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-base font-extrabold text-slate-900 mb-3">İlan Tarihi</h3>
|
||||
<div class="space-y-2 text-sm text-slate-700">
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="all" class="accent-rose-500" @checked($dateFilter === 'all')>
|
||||
<span>Tümü</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="today" class="accent-rose-500" @checked($dateFilter === 'today')>
|
||||
<span>Bugün</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="week" class="accent-rose-500" @checked($dateFilter === 'week')>
|
||||
<span>Son 7 Gün</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="date_filter" value="month" class="accent-rose-500" @checked($dateFilter === 'month')>
|
||||
<span>Son 30 Gün</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('listings.index', $clearFiltersQuery) }}" class="flex-1 h-10 inline-flex items-center justify-center rounded-full border border-rose-300 text-rose-500 text-sm font-semibold hover:bg-rose-50 transition">
|
||||
Temizle
|
||||
</a>
|
||||
<button type="submit" class="flex-1 h-10 rounded-full bg-rose-500 text-white text-sm font-semibold hover:bg-rose-600 transition">
|
||||
Uygula
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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">
|
||||
<p class="text-sm text-slate-700 mr-auto">
|
||||
{{ $activeCategoryName !== '' ? 'İkinci El '.$activeCategoryName.' kategorisinde' : 'İkinci El Araba kategorisinde' }}
|
||||
<strong>{{ number_format($resultListingsCount, 0, ',', '.') }}</strong>
|
||||
ilan bulundu
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.searches.store') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
<input type="hidden" name="category_id" value="{{ $categoryId }}">
|
||||
<button type="submit" class="h-10 px-4 rounded-full border text-sm font-semibold transition {{ $isCurrentSearchSaved ? 'bg-emerald-100 border-emerald-200 text-emerald-700 cursor-default' : ($canSaveSearch ? 'bg-rose-50 border-rose-200 text-rose-600 hover:bg-rose-100' : 'bg-slate-100 border-slate-200 text-slate-400 cursor-not-allowed') }}" @disabled($isCurrentSearchSaved || ! $canSaveSearch)>
|
||||
{{ $isCurrentSearchSaved ? 'Arama Kaydedildi' : 'Arama Kaydet' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="h-10 px-4 inline-flex items-center rounded-full border border-slate-300 text-sm font-semibold text-slate-600 hover:bg-slate-50 transition">
|
||||
Arama Kaydet
|
||||
</a>
|
||||
@endauth
|
||||
|
||||
<form method="GET" action="{{ route('listings.index') }}">
|
||||
@if($search !== '')
|
||||
<input type="hidden" name="search" value="{{ $search }}">
|
||||
@endif
|
||||
@if($categoryId)
|
||||
<input type="hidden" name="category" value="{{ $categoryId }}">
|
||||
@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="h-10 px-4 rounded-full border border-slate-300 bg-white inline-flex items-center gap-2 text-sm font-semibold text-slate-700">
|
||||
<span>Akıllı Sıralama</span>
|
||||
<select name="sort" class="bg-transparent text-sm font-semibold focus:outline-none" onchange="this.form.submit()">
|
||||
<option value="smart" @selected($sort === 'smart')>Önerilen</option>
|
||||
<option value="newest" @selected($sort === 'newest')>En Yeni</option>
|
||||
<option value="oldest" @selected($sort === 'oldest')>En Eski</option>
|
||||
<option value="price_asc" @selected($sort === 'price_asc')>Fiyat Artan</option>
|
||||
<option value="price_desc" @selected($sort === 'price_desc')>Fiyat Azalan</option>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($listings->isEmpty())
|
||||
<div class="listing-filter-card py-14 text-center text-slate-500">
|
||||
Bu filtreye uygun ilan bulunamadı.
|
||||
</div>
|
||||
@else
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3.5">
|
||||
@foreach($listings as $listing)
|
||||
@php
|
||||
$listingImage = $listing->getFirstMediaUrl('listing-images');
|
||||
$isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
|
||||
$priceValue = ! is_null($listing->price) ? (float) $listing->price : null;
|
||||
$locationParts = array_filter([
|
||||
trim((string) ($listing->city ?? '')),
|
||||
trim((string) ($listing->country ?? '')),
|
||||
], fn ($value) => $value !== '');
|
||||
$locationText = implode(', ', $locationParts);
|
||||
@endphp
|
||||
<article class="listing-card">
|
||||
<div class="relative h-52 bg-slate-200">
|
||||
@if($listingImage)
|
||||
<a href="{{ route('listings.show', $listing) }}" class="block w-full h-full">
|
||||
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover">
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('listings.show', $listing) }}" class="w-full h-full grid place-items-center text-slate-400">
|
||||
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 16l4.6-4.6a2 2 0 012.8 0L16 16m-2-2 1.6-1.6a2 2 0 012.8 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@if($listing->is_featured)
|
||||
<span class="absolute top-2 left-2 inline-flex items-center rounded-full bg-yellow-300 text-slate-900 text-[11px] font-bold px-2.5 py-1">
|
||||
Öne Çıkan
|
||||
</span>
|
||||
@endif
|
||||
|
||||
<div class="absolute top-2 right-2">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="w-8 h-8 rounded-full grid place-items-center transition {{ $isFavorited ? 'bg-rose-500 text-white' : 'bg-white text-slate-500 hover:text-rose-500' }}" aria-label="Favoriye ekle">
|
||||
♥
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="w-8 h-8 rounded-full bg-white text-slate-500 hover:text-rose-500 grid place-items-center transition" aria-label="Giriş yap">
|
||||
♥
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
@if(!is_null($priceValue) && $priceValue > 0)
|
||||
{{ number_format($priceValue, 0, ',', '.') }} {{ $listing->currency }}
|
||||
@else
|
||||
Ücretsiz
|
||||
@endif
|
||||
</p>
|
||||
<h3 class="listing-title mt-2 text-sm font-semibold text-slate-900">
|
||||
{{ $listing->title }}
|
||||
</h3>
|
||||
</a>
|
||||
|
||||
<p class="text-xs text-slate-500 mt-2">
|
||||
{{ $listing->category?->name ?: 'Kategori yok' }}
|
||||
</p>
|
||||
|
||||
<div class="mt-3 pt-2 border-t border-slate-100 flex items-center justify-between gap-2 text-[12px] text-slate-500">
|
||||
<span class="truncate">{{ $locationText !== '' ? $locationText : 'Konum belirtilmedi' }}</span>
|
||||
<span class="shrink-0">{{ $listing->created_at?->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pt-2">
|
||||
{{ $listings->links() }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const countrySelect = document.querySelector('[data-listing-country]');
|
||||
const citySelect = document.querySelector('[data-listing-city]');
|
||||
const currentLocationButton = document.querySelector('[data-use-current-location]');
|
||||
const citiesTemplate = countrySelect?.dataset.citiesUrlTemplate ?? '';
|
||||
const locationStorageKey = 'oc2.header.location';
|
||||
|
||||
if (!countrySelect || !citySelect || citiesTemplate === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalize = (value) => (value ?? '')
|
||||
.toString()
|
||||
.toLocaleLowerCase('tr-TR')
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.trim();
|
||||
|
||||
const setCityOptions = (cities, selectedCityName = '') => {
|
||||
citySelect.innerHTML = '<option value="">İlçe seçin</option>';
|
||||
cities.forEach((city) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = String(city.id ?? '');
|
||||
option.textContent = city.name ?? '';
|
||||
option.dataset.name = city.name ?? '';
|
||||
citySelect.appendChild(option);
|
||||
});
|
||||
citySelect.disabled = false;
|
||||
|
||||
if (selectedCityName) {
|
||||
const matched = Array.from(citySelect.options).find((option) => normalize(option.dataset.name) === normalize(selectedCityName));
|
||||
if (matched) {
|
||||
citySelect.value = matched.value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCityOptions = async (url) => {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('city_fetch_failed');
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
if (Array.isArray(payload)) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
return Array.isArray(payload?.data) ? payload.data : [];
|
||||
};
|
||||
|
||||
const loadCities = async (countryId, selectedCityName = '') => {
|
||||
if (!countryId) {
|
||||
citySelect.innerHTML = '<option value="">Önce il seçin</option>';
|
||||
citySelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
citySelect.disabled = true;
|
||||
citySelect.innerHTML = '<option value="">İlçeler yükleniyor...</option>';
|
||||
|
||||
const primaryUrl = citiesTemplate.replace('__COUNTRY__', encodeURIComponent(String(countryId)));
|
||||
|
||||
try {
|
||||
let cities = [];
|
||||
|
||||
try {
|
||||
cities = await fetchCityOptions(primaryUrl);
|
||||
} catch (primaryError) {
|
||||
if (!/^https?:\/\//i.test(primaryUrl)) {
|
||||
throw primaryError;
|
||||
}
|
||||
|
||||
let fallbackUrl = null;
|
||||
|
||||
try {
|
||||
const parsed = new URL(primaryUrl);
|
||||
fallbackUrl = `${parsed.pathname}${parsed.search}`;
|
||||
} catch (urlError) {
|
||||
fallbackUrl = null;
|
||||
}
|
||||
|
||||
if (!fallbackUrl) {
|
||||
throw primaryError;
|
||||
}
|
||||
|
||||
cities = await fetchCityOptions(fallbackUrl);
|
||||
}
|
||||
|
||||
setCityOptions(cities, selectedCityName);
|
||||
} catch (error) {
|
||||
citySelect.innerHTML = '<option value="">İlçeler yüklenemedi</option>';
|
||||
citySelect.disabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
countrySelect.addEventListener('change', () => {
|
||||
citySelect.value = '';
|
||||
void loadCities(countrySelect.value);
|
||||
});
|
||||
|
||||
currentLocationButton?.addEventListener('click', async () => {
|
||||
try {
|
||||
const rawLocation = localStorage.getItem(locationStorageKey);
|
||||
if (!rawLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedLocation = JSON.parse(rawLocation);
|
||||
const countryName = parsedLocation?.countryName ?? '';
|
||||
const cityName = parsedLocation?.cityName ?? '';
|
||||
const countryId = parsedLocation?.countryId ? String(parsedLocation.countryId) : null;
|
||||
|
||||
const matchedCountryOption = Array.from(countrySelect.options).find((option) => {
|
||||
if (countryId && option.value === countryId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return normalize(option.textContent) === normalize(countryName);
|
||||
});
|
||||
|
||||
if (!matchedCountryOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
countrySelect.value = matchedCountryOption.value;
|
||||
await loadCities(matchedCountryOption.value, cityName);
|
||||
} catch (error) {
|
||||
// no-op
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@include('listing::partials.index-content')
|
||||
@endsection
|
||||
|
||||
@ -40,6 +40,12 @@
|
||||
$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;
|
||||
$canStartConversation = auth()->check() && $listing->user && ! $isOwnListing;
|
||||
$loginRedirectRoute = route('login', ['redirect' => request()->fullUrl()]);
|
||||
$chatConversation = $detailConversation ?? null;
|
||||
$chatMessages = $chatConversation?->messages ?? collect();
|
||||
$chatSendUrl = $chatConversation ? route('conversations.messages.send', $chatConversation) : '';
|
||||
$chatStartUrl = route('conversations.start', $listing);
|
||||
|
||||
$primaryContactHref = null;
|
||||
$primaryContactLabel = 'Call';
|
||||
@ -51,13 +57,6 @@
|
||||
$primaryContactLabel = 'Email';
|
||||
}
|
||||
|
||||
$mapQuery = filled($listing->latitude) && filled($listing->longitude)
|
||||
? trim((string) $listing->latitude).','.trim((string) $listing->longitude)
|
||||
: 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 '.$referenceCode);
|
||||
$shareUrl = route('listings.show', $listing);
|
||||
@ -358,21 +357,12 @@
|
||||
<div class="lt-row-2">
|
||||
@if(! $listing->user)
|
||||
<button type="button" class="lt-btn" disabled>Unavailable</button>
|
||||
@elseif($canContactSeller)
|
||||
@if($existingConversationId)
|
||||
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="lt-btn">
|
||||
Message
|
||||
</a>
|
||||
@else
|
||||
<form method="POST" action="{{ route('conversations.start', $listing) }}" class="lt-action-form">
|
||||
@csrf
|
||||
<button type="submit" class="lt-btn">Message</button>
|
||||
</form>
|
||||
@endif
|
||||
@elseif($canStartConversation)
|
||||
<button type="button" class="lt-btn" data-inline-chat-open>Message</button>
|
||||
@elseif($isOwnListing)
|
||||
<button type="button" class="lt-btn" disabled>Your listing</button>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="lt-btn">Message</a>
|
||||
<a href="{{ $loginRedirectRoute }}" class="lt-btn">Message</a>
|
||||
@endif
|
||||
|
||||
@if($primaryContactHref)
|
||||
@ -382,50 +372,22 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if(! $listing->user)
|
||||
<button type="button" class="lt-btn lt-btn-main" disabled>Unavailable</button>
|
||||
@elseif($canContactSeller)
|
||||
@if($existingConversationId)
|
||||
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="lt-btn lt-btn-main">
|
||||
Make offer
|
||||
</a>
|
||||
@else
|
||||
<form method="POST" action="{{ route('conversations.start', $listing) }}" class="lt-action-form">
|
||||
@csrf
|
||||
<button type="submit" class="lt-btn lt-btn-main">Make offer</button>
|
||||
</form>
|
||||
@endif
|
||||
@elseif($isOwnListing)
|
||||
<button type="button" class="lt-btn lt-btn-main" disabled>Manage listing</button>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="lt-btn lt-btn-main">Make offer</a>
|
||||
@endif
|
||||
|
||||
<div class="lt-row-2">
|
||||
@if($mapUrl)
|
||||
<a href="{{ $mapUrl }}" target="_blank" rel="noreferrer" class="lt-btn lt-btn-outline">
|
||||
View map
|
||||
</a>
|
||||
@else
|
||||
<button type="button" class="lt-btn lt-btn-outline" disabled>View map</button>
|
||||
@endif
|
||||
|
||||
@if($listing->user && ! $isOwnListing)
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}" class="lt-action-form">
|
||||
@csrf
|
||||
<input type="hidden" name="redirect_to" value="{{ request()->fullUrl() }}">
|
||||
<button type="submit" class="lt-btn lt-btn-outline">
|
||||
{{ $isSellerFavorited ? 'Saved seller' : 'Save seller' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="lt-btn lt-btn-outline">Save seller</a>
|
||||
<a href="{{ $loginRedirectRoute }}" class="lt-btn lt-btn-outline">Save seller</a>
|
||||
@endauth
|
||||
@else
|
||||
<button type="button" class="lt-btn lt-btn-outline" disabled>{{ $isOwnListing ? 'Your account' : 'Save seller' }}</button>
|
||||
@elseif($isOwnListing)
|
||||
<button type="button" class="lt-btn lt-btn-outline" disabled>Your account</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="lt-card lt-safety-card">
|
||||
@ -441,21 +403,12 @@
|
||||
<div class="lt-mobile-actions-row">
|
||||
@if(! $listing->user)
|
||||
<button type="button" class="lt-btn" disabled>Unavailable</button>
|
||||
@elseif($canContactSeller)
|
||||
@if($existingConversationId)
|
||||
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="lt-btn">
|
||||
Message
|
||||
</a>
|
||||
@else
|
||||
<form method="POST" action="{{ route('conversations.start', $listing) }}" class="lt-action-form">
|
||||
@csrf
|
||||
<button type="submit" class="lt-btn">Message</button>
|
||||
</form>
|
||||
@endif
|
||||
@elseif($canStartConversation)
|
||||
<button type="button" class="lt-btn" data-inline-chat-open>Message</button>
|
||||
@elseif($isOwnListing)
|
||||
<button type="button" class="lt-btn" disabled>Your listing</button>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="lt-btn">Message</a>
|
||||
<a href="{{ $loginRedirectRoute }}" class="lt-btn">Message</a>
|
||||
@endif
|
||||
|
||||
@if($primaryContactHref)
|
||||
@ -465,27 +418,76 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if(! $listing->user)
|
||||
<button type="button" class="lt-btn lt-btn-main" disabled>Unavailable</button>
|
||||
@elseif($canContactSeller)
|
||||
@if($existingConversationId)
|
||||
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="lt-btn lt-btn-main">
|
||||
Make offer
|
||||
</a>
|
||||
@else
|
||||
<form method="POST" action="{{ route('conversations.start', $listing) }}" class="lt-action-form">
|
||||
@if($listing->user && ! $isOwnListing)
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}" class="lt-action-form">
|
||||
@csrf
|
||||
<button type="submit" class="lt-btn lt-btn-main">Make offer</button>
|
||||
<input type="hidden" name="redirect_to" value="{{ request()->fullUrl() }}">
|
||||
<button type="submit" class="lt-btn lt-btn-outline">
|
||||
{{ $isSellerFavorited ? 'Saved seller' : 'Save seller' }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@elseif($isOwnListing)
|
||||
<button type="button" class="lt-btn lt-btn-main" disabled>Manage listing</button>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="lt-btn lt-btn-main">Make offer</a>
|
||||
<a href="{{ $loginRedirectRoute }}" class="lt-btn lt-btn-outline">Save seller</a>
|
||||
@endauth
|
||||
@elseif($isOwnListing)
|
||||
<button type="button" class="lt-btn lt-btn-outline" disabled>Your account</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($canStartConversation)
|
||||
<div class="lt-chat-widget" data-inline-chat data-start-url="{{ $chatStartUrl }}" data-send-url="{{ $chatSendUrl }}">
|
||||
<section class="lt-chat-panel" data-inline-chat-panel hidden>
|
||||
<div class="lt-chat-head">
|
||||
<div>
|
||||
<p class="lt-chat-kicker">Chat</p>
|
||||
<p class="lt-chat-name">{{ $sellerName }}</p>
|
||||
<p class="lt-chat-meta">{{ $displayTitle }}</p>
|
||||
</div>
|
||||
<button type="button" class="lt-chat-close" data-inline-chat-close aria-label="Close chat">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||
<path d="M6 6 18 18M18 6 6 18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="lt-chat-thread" data-inline-chat-thread>
|
||||
@foreach($chatMessages as $message)
|
||||
<div class="lt-chat-item {{ (int) $message->sender_id === (int) auth()->id() ? 'is-mine' : '' }}">
|
||||
<div class="lt-chat-bubble">{{ $message->body }}</div>
|
||||
<span class="lt-chat-time">{{ $message->created_at?->format('H:i') }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<div class="lt-chat-empty {{ $chatMessages->isNotEmpty() ? 'is-hidden' : '' }}" data-inline-chat-empty>
|
||||
Send the first message without leaving this page.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="lt-chat-form" data-inline-chat-form>
|
||||
<input
|
||||
type="text"
|
||||
name="message"
|
||||
class="lt-chat-input"
|
||||
data-inline-chat-input
|
||||
maxlength="2000"
|
||||
placeholder="Write a message"
|
||||
autocomplete="off"
|
||||
required
|
||||
>
|
||||
<button type="submit" class="lt-chat-send" data-inline-chat-submit aria-label="Send message">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h13m0 0-5-5m5 5-5 5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="lt-chat-error is-hidden" data-inline-chat-error></p>
|
||||
</section>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(($relatedListings ?? collect())->isNotEmpty() || ($themePillCategories ?? collect())->isNotEmpty())
|
||||
<section class="lt-related">
|
||||
@if(($relatedListings ?? collect())->isNotEmpty())
|
||||
@ -677,6 +679,138 @@
|
||||
button.addEventListener('click', () => activate(button.dataset.tab || 'details'));
|
||||
});
|
||||
});
|
||||
|
||||
const chatRoot = document.querySelector('[data-inline-chat]');
|
||||
if (chatRoot) {
|
||||
const panel = chatRoot.querySelector('[data-inline-chat-panel]');
|
||||
const thread = chatRoot.querySelector('[data-inline-chat-thread]');
|
||||
const emptyState = chatRoot.querySelector('[data-inline-chat-empty]');
|
||||
const form = chatRoot.querySelector('[data-inline-chat-form]');
|
||||
const input = chatRoot.querySelector('[data-inline-chat-input]');
|
||||
const error = chatRoot.querySelector('[data-inline-chat-error]');
|
||||
const submitButton = chatRoot.querySelector('[data-inline-chat-submit]');
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
|
||||
const togglePanel = (open) => {
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
panel.hidden = !open;
|
||||
chatRoot.classList.toggle('is-open', open);
|
||||
|
||||
if (open) {
|
||||
window.requestAnimationFrame(() => input?.focus());
|
||||
}
|
||||
};
|
||||
|
||||
const showError = (message) => {
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
error.textContent = '';
|
||||
error.classList.add('is-hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
error.textContent = message;
|
||||
error.classList.remove('is-hidden');
|
||||
};
|
||||
|
||||
const appendMessage = (message) => {
|
||||
if (!thread || !message?.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'lt-chat-item' + (message.is_mine ? ' is-mine' : '');
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'lt-chat-bubble';
|
||||
bubble.textContent = message.body;
|
||||
|
||||
const time = document.createElement('span');
|
||||
time.className = 'lt-chat-time';
|
||||
time.textContent = message.time || '';
|
||||
|
||||
item.appendChild(bubble);
|
||||
item.appendChild(time);
|
||||
thread.appendChild(item);
|
||||
thread.scrollTop = thread.scrollHeight;
|
||||
emptyState?.classList.add('is-hidden');
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-inline-chat-open]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
showError('');
|
||||
togglePanel(true);
|
||||
});
|
||||
});
|
||||
|
||||
chatRoot.querySelector('[data-inline-chat-close]')?.addEventListener('click', () => {
|
||||
togglePanel(false);
|
||||
});
|
||||
|
||||
form?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!input || !submitButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = input.value.trim();
|
||||
if (message === '') {
|
||||
showError('Message cannot be empty.');
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUrl = chatRoot.dataset.sendUrl || chatRoot.dataset.startUrl;
|
||||
if (!targetUrl) {
|
||||
showError('Messaging is not available right now.');
|
||||
return;
|
||||
}
|
||||
|
||||
showError('');
|
||||
submitButton.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body: new URLSearchParams({ message }).toString(),
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
const responseMessage = payload?.message || payload?.errors?.message?.[0] || 'Message could not be sent.';
|
||||
throw new Error(responseMessage);
|
||||
}
|
||||
|
||||
if (payload.send_url) {
|
||||
chatRoot.dataset.sendUrl = payload.send_url;
|
||||
}
|
||||
|
||||
if (payload.message) {
|
||||
appendMessage(payload.message);
|
||||
}
|
||||
|
||||
input.value = '';
|
||||
input.focus();
|
||||
} catch (requestError) {
|
||||
showError(requestError instanceof Error ? requestError.message : 'Message could not be sent.');
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@ -57,12 +57,12 @@ class LocationSeeder extends Seeder
|
||||
if ($value === 'us_ca') {
|
||||
$countries['US'] = [
|
||||
'code' => 'US',
|
||||
'name' => 'Amerika Birleşik Devletleri',
|
||||
'name' => 'United States',
|
||||
'phone_code' => $phoneCode,
|
||||
];
|
||||
$countries['CA'] = [
|
||||
'code' => 'CA',
|
||||
'name' => 'Kanada',
|
||||
'name' => 'Canada',
|
||||
'phone_code' => $phoneCode,
|
||||
];
|
||||
|
||||
@ -72,12 +72,12 @@ class LocationSeeder extends Seeder
|
||||
if ($value === 'ru_kz') {
|
||||
$countries['RU'] = [
|
||||
'code' => 'RU',
|
||||
'name' => 'Rusya',
|
||||
'name' => 'Russia',
|
||||
'phone_code' => $phoneCode,
|
||||
];
|
||||
$countries['KZ'] = [
|
||||
'code' => 'KZ',
|
||||
'name' => 'Kazakistan',
|
||||
'name' => 'Kazakhstan',
|
||||
'phone_code' => $phoneCode,
|
||||
];
|
||||
|
||||
@ -85,12 +85,9 @@ class LocationSeeder extends Seeder
|
||||
}
|
||||
|
||||
$key = 'filament-country-code-field::countries.' . $value;
|
||||
$labelTr = trim((string) trans($key, [], 'tr'));
|
||||
$labelEn = trim((string) trans($key, [], 'en'));
|
||||
|
||||
$name = $labelTr !== '' && $labelTr !== $key
|
||||
? $labelTr
|
||||
: ($labelEn !== '' && $labelEn !== $key ? $labelEn : strtoupper($value));
|
||||
$name = $labelEn !== '' && $labelEn !== $key ? $labelEn : strtoupper($value);
|
||||
|
||||
$iso2 = strtoupper(explode('_', $value)[0] ?? $value);
|
||||
|
||||
@ -122,67 +119,67 @@ class LocationSeeder extends Seeder
|
||||
{
|
||||
return [
|
||||
'Adana',
|
||||
'Adıyaman',
|
||||
'Adiyaman',
|
||||
'Afyonkarahisar',
|
||||
'Ağrı',
|
||||
'Agri',
|
||||
'Aksaray',
|
||||
'Amasya',
|
||||
'Ankara',
|
||||
'Antalya',
|
||||
'Ardahan',
|
||||
'Artvin',
|
||||
'Aydın',
|
||||
'Balıkesir',
|
||||
'Bartın',
|
||||
'Aydin',
|
||||
'Balikesir',
|
||||
'Bartin',
|
||||
'Batman',
|
||||
'Bayburt',
|
||||
'Bilecik',
|
||||
'Bingöl',
|
||||
'Bingol',
|
||||
'Bitlis',
|
||||
'Bolu',
|
||||
'Burdur',
|
||||
'Bursa',
|
||||
'Çanakkale',
|
||||
'Çankırı',
|
||||
'Çorum',
|
||||
'Canakkale',
|
||||
'Cankiri',
|
||||
'Corum',
|
||||
'Denizli',
|
||||
'Diyarbakır',
|
||||
'Düzce',
|
||||
'Diyarbakir',
|
||||
'Duzce',
|
||||
'Edirne',
|
||||
'Elazığ',
|
||||
'Elazig',
|
||||
'Erzincan',
|
||||
'Erzurum',
|
||||
'Eskişehir',
|
||||
'Eskisehir',
|
||||
'Gaziantep',
|
||||
'Giresun',
|
||||
'Gümüşhane',
|
||||
'Gumushane',
|
||||
'Hakkari',
|
||||
'Hatay',
|
||||
'Iğdır',
|
||||
'Igdir',
|
||||
'Isparta',
|
||||
'İstanbul',
|
||||
'İzmir',
|
||||
'Kahramanmaraş',
|
||||
'Karabük',
|
||||
'Istanbul',
|
||||
'Izmir',
|
||||
'Kahramanmaras',
|
||||
'Karabuk',
|
||||
'Karaman',
|
||||
'Kars',
|
||||
'Kastamonu',
|
||||
'Kayseri',
|
||||
'Kilis',
|
||||
'Kırıkkale',
|
||||
'Kırklareli',
|
||||
'Kırşehir',
|
||||
'Kirikkale',
|
||||
'Kirklareli',
|
||||
'Kirsehir',
|
||||
'Kocaeli',
|
||||
'Konya',
|
||||
'Kütahya',
|
||||
'Kutahya',
|
||||
'Malatya',
|
||||
'Manisa',
|
||||
'Mardin',
|
||||
'Mersin',
|
||||
'Muğla',
|
||||
'Muş',
|
||||
'Nevşehir',
|
||||
'Niğde',
|
||||
'Mugla',
|
||||
'Mus',
|
||||
'Nevsehir',
|
||||
'Nigde',
|
||||
'Ordu',
|
||||
'Osmaniye',
|
||||
'Rize',
|
||||
@ -191,13 +188,13 @@ class LocationSeeder extends Seeder
|
||||
'Siirt',
|
||||
'Sinop',
|
||||
'Sivas',
|
||||
'Şanlıurfa',
|
||||
'Şırnak',
|
||||
'Tekirdağ',
|
||||
'Sanliurfa',
|
||||
'Sirnak',
|
||||
'Tekirdag',
|
||||
'Tokat',
|
||||
'Trabzon',
|
||||
'Tunceli',
|
||||
'Uşak',
|
||||
'Usak',
|
||||
'Van',
|
||||
'Yalova',
|
||||
'Yozgat',
|
||||
|
||||
@ -24,7 +24,7 @@ class AuthUserSeeder extends Seeder
|
||||
['email' => 'b@b.com'],
|
||||
[
|
||||
'name' => 'Member',
|
||||
'password' => '36330',
|
||||
'password' => '236330',
|
||||
'status' => 'active',
|
||||
],
|
||||
);
|
||||
|
||||
@ -207,7 +207,7 @@ class PanelController extends Controller
|
||||
$this->guardListingOwner($request, $listing);
|
||||
$listing->delete();
|
||||
|
||||
return back()->with('success', 'İlan kaldırıldı.');
|
||||
return back()->with('success', 'Listing removed.');
|
||||
}
|
||||
|
||||
public function markListingAsSold(Request $request, Listing $listing): RedirectResponse
|
||||
@ -217,7 +217,7 @@ class PanelController extends Controller
|
||||
'status' => 'sold',
|
||||
])->save();
|
||||
|
||||
return back()->with('success', 'İlan satıldı olarak işaretlendi.');
|
||||
return back()->with('success', 'Listing marked as sold.');
|
||||
}
|
||||
|
||||
public function republishListing(Request $request, Listing $listing): RedirectResponse
|
||||
@ -228,7 +228,7 @@ class PanelController extends Controller
|
||||
'expires_at' => now()->addDays(30),
|
||||
])->save();
|
||||
|
||||
return back()->with('success', 'İlan yeniden yayına alındı.');
|
||||
return back()->with('success', 'Listing republished.');
|
||||
}
|
||||
|
||||
private function guardListingOwner(Request $request, Listing $listing): void
|
||||
|
||||
@ -514,13 +514,10 @@ class PanelQuickListingForm extends Component
|
||||
$fieldRules[] = $isRequired ? 'required' : 'nullable';
|
||||
}
|
||||
|
||||
$fieldRules[] = match ($type) {
|
||||
ListingCustomField::TYPE_TEXT => 'string|max:255',
|
||||
ListingCustomField::TYPE_TEXTAREA => 'string|max:2000',
|
||||
ListingCustomField::TYPE_NUMBER => 'numeric',
|
||||
ListingCustomField::TYPE_DATE => 'date',
|
||||
default => 'sometimes',
|
||||
};
|
||||
$fieldRules = [
|
||||
...$fieldRules,
|
||||
...$this->customFieldTypeRules($type),
|
||||
];
|
||||
|
||||
if ($type === ListingCustomField::TYPE_SELECT) {
|
||||
$options = collect($field['options'] ?? [])->map(fn ($option): string => (string) $option)->all();
|
||||
@ -535,6 +532,17 @@ class PanelQuickListingForm extends Component
|
||||
}
|
||||
}
|
||||
|
||||
private function customFieldTypeRules(string $type): array
|
||||
{
|
||||
return match ($type) {
|
||||
ListingCustomField::TYPE_TEXT => ['string', 'max:255'],
|
||||
ListingCustomField::TYPE_TEXTAREA => ['string', 'max:2000'],
|
||||
ListingCustomField::TYPE_NUMBER => ['numeric'],
|
||||
ListingCustomField::TYPE_DATE => ['date'],
|
||||
default => ['sometimes'],
|
||||
};
|
||||
}
|
||||
|
||||
private function createListing(): Listing
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
@ -34,7 +34,7 @@ class AppServiceProvider extends ServiceProvider
|
||||
$availableLocales = config('app.available_locales', ['en']);
|
||||
$localeLabels = [
|
||||
'en' => 'English',
|
||||
'tr' => 'Türkçe',
|
||||
'tr' => 'Turkish',
|
||||
];
|
||||
|
||||
LanguageSwitch::configureUsing(function (LanguageSwitch $switch) use ($availableLocales, $localeLabels): void {
|
||||
|
||||
@ -1314,6 +1314,194 @@ summary::-webkit-details-marker {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lt-chat-widget {
|
||||
position: fixed;
|
||||
right: 22px;
|
||||
bottom: 22px;
|
||||
z-index: 55;
|
||||
}
|
||||
|
||||
.lt-chat-panel {
|
||||
width: min(380px, calc(100vw - 28px));
|
||||
display: grid;
|
||||
gap: 0;
|
||||
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||
border-radius: 24px;
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
box-shadow: 0 26px 60px rgba(15, 23, 42, 0.18);
|
||||
backdrop-filter: saturate(180%) blur(18px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lt-chat-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 18px 14px;
|
||||
border-bottom: 1px solid rgba(29, 29, 31, 0.08);
|
||||
background: linear-gradient(180deg, rgba(0, 113, 227, 0.06) 0%, rgba(255, 255, 255, 0.98) 100%);
|
||||
}
|
||||
|
||||
.lt-chat-kicker {
|
||||
margin: 0 0 4px;
|
||||
color: var(--oc-primary);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.lt-chat-name {
|
||||
margin: 0;
|
||||
color: var(--oc-text);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lt-chat-meta {
|
||||
margin: 4px 0 0;
|
||||
color: #667085;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.lt-chat-close {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: #475467;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.lt-chat-close:hover {
|
||||
background: #f8fafc;
|
||||
color: var(--oc-text);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.lt-chat-thread {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-height: 260px;
|
||||
max-height: 360px;
|
||||
padding: 16px 16px 12px;
|
||||
overflow-y: auto;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.lt-chat-item {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.lt-chat-item.is-mine {
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.lt-chat-bubble {
|
||||
max-width: 88%;
|
||||
padding: 11px 14px;
|
||||
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||
border-radius: 18px;
|
||||
background: #ffffff;
|
||||
color: var(--oc-text);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.6;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.lt-chat-item.is-mine .lt-chat-bubble {
|
||||
border-color: rgba(0, 113, 227, 0.14);
|
||||
background: rgba(0, 113, 227, 0.09);
|
||||
color: var(--oc-primary);
|
||||
}
|
||||
|
||||
.lt-chat-time {
|
||||
color: #98a2b3;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lt-chat-empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 100%;
|
||||
padding: 18px;
|
||||
color: #667085;
|
||||
font-size: 0.88rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lt-chat-error {
|
||||
margin: 0;
|
||||
padding: 0 18px 14px;
|
||||
color: #dc2626;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lt-chat-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 48px;
|
||||
gap: 10px;
|
||||
padding: 14px 16px 16px;
|
||||
border-top: 1px solid rgba(29, 29, 31, 0.08);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
.lt-chat-input {
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid rgba(29, 29, 31, 0.12);
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: var(--oc-text);
|
||||
font-size: 0.92rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.lt-chat-input:focus {
|
||||
border-color: rgba(0, 113, 227, 0.24);
|
||||
box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.12);
|
||||
}
|
||||
|
||||
.lt-chat-send {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(180deg, #1581eb 0%, #0071e3 100%);
|
||||
color: #ffffff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.lt-chat-send:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 14px 28px rgba(0, 113, 227, 0.2);
|
||||
}
|
||||
|
||||
.lt-chat-send:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.lt-chat-empty.is-hidden,
|
||||
.lt-chat-error.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lt-mobile-actions {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
@ -1860,6 +2048,22 @@ summary::-webkit-details-marker {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lt-chat-widget {
|
||||
right: 10px;
|
||||
bottom: calc(104px + env(safe-area-inset-bottom, 0px));
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.lt-chat-panel {
|
||||
width: 100%;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.lt-chat-thread {
|
||||
min-height: 220px;
|
||||
max-height: 44vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
|
||||
@ -1,7 +1 @@
|
||||
import './bootstrap';
|
||||
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
window.Alpine = Alpine;
|
||||
|
||||
Alpine.start();
|
||||
|
||||
@ -10,23 +10,23 @@
|
||||
<body class="min-h-screen font-sans antialiased bg-gray-100 flex items-center justify-center p-6">
|
||||
<div class="max-w-md w-full bg-white rounded-xl shadow p-6 text-center">
|
||||
<h1 class="text-2xl font-bold text-gray-900">403</h1>
|
||||
<p class="mt-2 text-gray-700">Bu sayfaya erişim izniniz yok.</p>
|
||||
<p class="mt-2 text-gray-700">You do not have permission to access this page.</p>
|
||||
|
||||
<div class="mt-6 flex items-center justify-center gap-3">
|
||||
<a href="{{ route('home') }}" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50">
|
||||
Ana Sayfa
|
||||
Home
|
||||
</a>
|
||||
|
||||
@auth
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700">
|
||||
Çıkış Yap
|
||||
Log out
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ route('login') }}" class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700">
|
||||
Giriş Yap
|
||||
Log in
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
@ -138,7 +138,7 @@
|
||||
type="button"
|
||||
data-home-slide-prev
|
||||
class="w-8 h-8 rounded-full border border-white/45 text-white grid place-items-center hover:bg-white/15 transition"
|
||||
aria-label="Önceki slide"
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
<span aria-hidden="true">‹</span>
|
||||
</button>
|
||||
@ -158,7 +158,7 @@
|
||||
type="button"
|
||||
data-home-slide-next
|
||||
class="w-8 h-8 rounded-full border border-white/45 text-white grid place-items-center hover:bg-white/15 transition"
|
||||
aria-label="Sonraki slide"
|
||||
aria-label="Next slide"
|
||||
>
|
||||
<span aria-hidden="true">›</span>
|
||||
</button>
|
||||
@ -216,9 +216,9 @@
|
||||
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-3xl font-extrabold tracking-tight text-slate-900">Trend Kategoriler</h2>
|
||||
<h2 class="text-3xl font-extrabold tracking-tight text-slate-900">Trending Categories</h2>
|
||||
<a href="{{ route('categories.index') }}" class="hidden sm:inline-flex text-sm font-semibold text-rose-500 hover:text-rose-600 transition">
|
||||
Tümünü Gör
|
||||
View all
|
||||
</a>
|
||||
</div>
|
||||
<div class="relative">
|
||||
@ -226,7 +226,7 @@
|
||||
type="button"
|
||||
data-trend-prev
|
||||
class="hidden lg:inline-flex absolute left-0 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10 w-11 h-11 rounded-full border border-slate-300 bg-white text-slate-700 items-center justify-center shadow-sm hover:bg-slate-50 transition"
|
||||
aria-label="Önceki trend kategori"
|
||||
aria-label="Previous trending category"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 18-6-6 6-6"/>
|
||||
@ -296,7 +296,7 @@
|
||||
type="button"
|
||||
data-trend-next
|
||||
class="hidden lg:inline-flex absolute right-0 top-1/2 translate-x-1/2 -translate-y-1/2 z-10 w-11 h-11 rounded-full border border-slate-300 bg-white text-slate-700 items-center justify-center shadow-sm hover:bg-slate-50 transition"
|
||||
aria-label="Sonraki trend kategori"
|
||||
aria-label="Next trending category"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 18 6-6-6-6"/>
|
||||
@ -307,7 +307,7 @@
|
||||
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-2xl font-bold text-slate-900">Popüler İkinci El İlanlar</h2>
|
||||
<h2 class="text-2xl font-bold text-slate-900">Popular Listings</h2>
|
||||
<div class="hidden sm:flex items-center gap-2 text-sm text-slate-500">
|
||||
<span class="w-8 h-8 rounded-full border border-slate-300 grid place-items-center">‹</span>
|
||||
<span class="w-8 h-8 rounded-full border border-slate-300 grid place-items-center">›</span>
|
||||
@ -336,9 +336,9 @@
|
||||
</a>
|
||||
<div class="absolute top-3 left-3 flex items-center gap-2">
|
||||
@if($listing->is_featured)
|
||||
<span class="bg-amber-300 text-amber-950 text-xs font-bold px-2.5 py-1 rounded-full">Öne Çıkan</span>
|
||||
<span class="bg-amber-300 text-amber-950 text-xs font-bold px-2.5 py-1 rounded-full">Featured</span>
|
||||
@endif
|
||||
<span class="bg-sky-500 text-white text-xs font-semibold px-2.5 py-1 rounded-full">Büyük İlan</span>
|
||||
<span class="bg-sky-500 text-white text-xs font-semibold px-2.5 py-1 rounded-full">Spotlight</span>
|
||||
</div>
|
||||
<div class="absolute top-3 right-3">
|
||||
@auth
|
||||
@ -357,17 +357,17 @@
|
||||
<p class="text-3xl font-extrabold tracking-tight text-slate-900">{{ $priceLabel }}</p>
|
||||
<h3 class="text-xl font-semibold text-slate-800 mt-1 truncate">{{ $listing->title }}</h3>
|
||||
</div>
|
||||
<span class="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded-full font-semibold">12 taksit</span>
|
||||
<span class="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded-full font-semibold">12 installments</span>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center justify-between text-sm text-slate-500">
|
||||
<span class="truncate">{{ $locationLabel !== '' ? $locationLabel : 'Konum belirtilmedi' }}</span>
|
||||
<span class="truncate">{{ $locationLabel !== '' ? $locationLabel : 'Location not specified' }}</span>
|
||||
<span>{{ $listing->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@empty
|
||||
<div class="col-span-2 border border-dashed border-slate-300 bg-white rounded-2xl py-20 text-center text-slate-500">
|
||||
Henüz ilan bulunmuyor.
|
||||
No listings yet.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
@ -377,15 +377,15 @@
|
||||
<div class="grid md:grid-cols-[1fr,auto] gap-6 items-center">
|
||||
<div>
|
||||
<h2 class="text-3xl md:text-4xl font-extrabold">{{ __('messages.sell_something') }}</h2>
|
||||
<p class="text-slate-300 mt-3">Dakikalar içinde ücretsiz ilan oluştur, binlerce alıcıya ulaş.</p>
|
||||
<p class="text-slate-300 mt-3">Create a free listing in minutes and reach thousands of buyers.</p>
|
||||
</div>
|
||||
@auth
|
||||
<a href="{{ route('panel.listings.create') }}" class="inline-flex items-center justify-center rounded-full bg-rose-500 hover:bg-rose-600 px-8 py-3 font-semibold transition whitespace-nowrap">
|
||||
Hemen İlan Ver
|
||||
Post listing
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('register') }}" class="inline-flex items-center justify-center rounded-full bg-white text-slate-900 hover:bg-slate-100 px-8 py-3 font-semibold transition whitespace-nowrap">
|
||||
Ücretsiz Başla
|
||||
Start free
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
@ -40,15 +40,15 @@
|
||||
$availableLocales = config('app.available_locales', ['en']);
|
||||
$localeLabels = [
|
||||
'en' => 'English',
|
||||
'tr' => 'Türkçe',
|
||||
'ar' => 'العربية',
|
||||
'zh' => '中文',
|
||||
'tr' => 'Turkish',
|
||||
'ar' => 'Arabic',
|
||||
'zh' => 'Chinese',
|
||||
'es' => 'Español',
|
||||
'fr' => 'Français',
|
||||
'de' => 'Deutsch',
|
||||
'pt' => 'Português',
|
||||
'ru' => 'Русский',
|
||||
'ja' => '日本語',
|
||||
'fr' => 'French',
|
||||
'de' => 'German',
|
||||
'pt' => 'Portuguese',
|
||||
'ru' => 'Russian',
|
||||
'ja' => 'Japanese',
|
||||
];
|
||||
$headerCategories = collect($headerNavCategories ?? [])->values();
|
||||
$menuBrowseLinks = collect([
|
||||
|
||||
@ -1,96 +1,76 @@
|
||||
@extends('app::layouts.app')
|
||||
|
||||
@section('title', 'İlanlarım')
|
||||
@section('title', 'My Listings')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$statusTabs = [
|
||||
[
|
||||
'key' => 'all',
|
||||
'label' => 'Tüm İlanlar',
|
||||
'label' => 'All Listings',
|
||||
'count' => (int) ($counts['all'] ?? 0),
|
||||
'description' => 'Hesabındaki tüm ilanlar',
|
||||
'description' => 'All listings in your account',
|
||||
],
|
||||
[
|
||||
'key' => 'sold',
|
||||
'label' => 'Satıldı',
|
||||
'label' => 'Sold',
|
||||
'count' => (int) ($counts['sold'] ?? 0),
|
||||
'description' => 'Kapanan satışlar',
|
||||
'description' => 'Closed sales',
|
||||
],
|
||||
[
|
||||
'key' => 'expired',
|
||||
'label' => 'Süresi Doldu',
|
||||
'label' => 'Expired',
|
||||
'count' => (int) ($counts['expired'] ?? 0),
|
||||
'description' => 'Yeniden yayın bekleyenler',
|
||||
'description' => 'Waiting to be republished',
|
||||
],
|
||||
];
|
||||
$overviewCards = [
|
||||
[
|
||||
'label' => 'Toplam İlan',
|
||||
'label' => 'Total Listings',
|
||||
'value' => (int) ($counts['all'] ?? 0),
|
||||
'hint' => 'Panelindeki tüm kayıtlar',
|
||||
'hint' => 'Every listing in your panel',
|
||||
],
|
||||
[
|
||||
'label' => 'Yayında',
|
||||
'label' => 'Live',
|
||||
'value' => (int) ($counts['active'] ?? 0),
|
||||
'hint' => 'Şu anda ziyaretçilere açık',
|
||||
'hint' => 'Visible to visitors right now',
|
||||
],
|
||||
[
|
||||
'label' => 'Satıldı',
|
||||
'label' => 'Sold',
|
||||
'value' => (int) ($counts['sold'] ?? 0),
|
||||
'hint' => 'Satışla kapanan ilanlar',
|
||||
'hint' => 'Listings closed by sale',
|
||||
],
|
||||
[
|
||||
'label' => 'Süresi Doldu',
|
||||
'label' => 'Expired',
|
||||
'value' => (int) ($counts['expired'] ?? 0),
|
||||
'hint' => 'Yeniden yayın bekleyen ilanlar',
|
||||
'hint' => 'Listings waiting to be republished',
|
||||
],
|
||||
];
|
||||
$hasFilters = $search !== '' || $status !== 'all';
|
||||
$pendingCount = (int) ($counts['pending'] ?? 0);
|
||||
@endphp
|
||||
|
||||
<div class="listings-dashboard-page mx-auto max-w-[1320px] px-4 py-6 md:py-8">
|
||||
<div class="grid gap-6 xl:grid-cols-[300px,minmax(0,1fr)]">
|
||||
<aside class="listings-dashboard-sidebar space-y-6">
|
||||
@include('panel.partials.sidebar', ['activeMenu' => 'listings'])
|
||||
|
||||
<div class="overflow-hidden rounded-[30px] border border-slate-200/80 bg-white/90 p-6 shadow-[0_20px_55px_rgba(15,23,42,0.08)] backdrop-blur">
|
||||
<p class="account-section-kicker">Kontrol Merkezi</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">İlanlarını tek bakışta yönet</h2>
|
||||
<p class="mt-3 text-sm leading-6 text-slate-500">
|
||||
Yayındaki, satılan ve süresi dolan ilanlarını daha hızlı filtrele. Arama alanı sadece gerekli yerde, aksiyonlar ise her kartta doğrudan görünür.
|
||||
</p>
|
||||
|
||||
<div class="mt-5 rounded-[24px] bg-slate-950 px-5 py-4 text-white shadow-[0_18px_38px_rgba(15,23,42,0.18)]">
|
||||
<p class="text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-slate-300">Bugün</p>
|
||||
<p class="mt-2 text-sm leading-6 text-slate-200">
|
||||
@if ($pendingCount > 0)
|
||||
{{ $pendingCount }} ilan moderasyon incelemesinde. Onaylanınca burada yayında olarak görünecek.
|
||||
@else
|
||||
Bu ekranda ilanlarının durumu, etkileşimi ve yayın süresi birlikte özetlenir.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="space-y-6">
|
||||
<div class="listings-dashboard-hero">
|
||||
<div class="min-w-0">
|
||||
<p class="account-section-kicker">Panel</p>
|
||||
<h1 class="mt-2 text-[2.3rem] font-semibold leading-tight tracking-[-0.04em] text-slate-950">İlanlarım</h1>
|
||||
<h1 class="mt-2 text-[2.3rem] font-semibold leading-tight tracking-[-0.04em] text-slate-950">My Listings</h1>
|
||||
<p class="mt-3 max-w-3xl text-sm leading-6 text-slate-500">
|
||||
Tüm ilanlarını tek ekranda takip et. Tarih, durum ve etkileşim bilgileri artık daha net; arama ve filtre alanı ise daha kompakt.
|
||||
Track all your listings from one screen. Dates, status, and engagement are clearer, while search and filters stay compact.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center">
|
||||
@if ($hasFilters)
|
||||
<a href="{{ route('panel.listings.index') }}" class="account-secondary-button">Filtreleri Temizle</a>
|
||||
<a href="{{ route('panel.listings.index') }}" class="account-secondary-button">Clear Filters</a>
|
||||
@endif
|
||||
|
||||
<a href="{{ route('panel.listings.create') }}" class="account-primary-button">Yeni İlan Ver</a>
|
||||
<a href="{{ route('panel.listings.create') }}" class="account-primary-button">New Listing</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -107,10 +87,10 @@
|
||||
<div class="listings-dashboard-filter-shell">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p class="account-section-kicker">Filtrele</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">Arama ve durum</h2>
|
||||
<p class="account-section-kicker">Filter</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">Search and status</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-slate-500">
|
||||
{{ number_format($listings->total()) }} sonuç içinde başlığa göre ara veya görünümü hızlıca daralt.
|
||||
Search by title or narrow the view within {{ number_format($listings->total()) }} results.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -122,11 +102,11 @@
|
||||
type="text"
|
||||
name="search"
|
||||
value="{{ $search }}"
|
||||
placeholder="İlan başlığına göre ara"
|
||||
placeholder="Search by listing title"
|
||||
class="listings-dashboard-search-input"
|
||||
>
|
||||
<input type="hidden" name="status" value="{{ $status }}">
|
||||
<button type="submit" class="listings-dashboard-search-button">Ara</button>
|
||||
<button type="submit" class="listings-dashboard-search-button">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -157,7 +137,7 @@
|
||||
$viewCount = (int) ($listing->view_count ?? 0);
|
||||
$publishedAt = $listing->panelPublishedAt();
|
||||
$publishedLabel = $publishedAt?->format('d.m.Y') ?? '-';
|
||||
$expiresLabel = $listing->expires_at?->format('d.m.Y') ?? 'Süresiz';
|
||||
$expiresLabel = $listing->expires_at?->format('M j, Y') ?? 'No expiry';
|
||||
$videoCount = (int) ($listing->videos_count ?? 0);
|
||||
$readyVideoCount = (int) ($listing->ready_videos_count ?? 0);
|
||||
$pendingVideoCount = (int) ($listing->pending_videos_count ?? 0);
|
||||
@ -170,7 +150,7 @@
|
||||
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="h-full w-full object-cover">
|
||||
@else
|
||||
<div class="listings-dashboard-placeholder">
|
||||
<span>Görsel Yok</span>
|
||||
<span>No image</span>
|
||||
</div>
|
||||
@endif
|
||||
</a>
|
||||
@ -198,21 +178,21 @@
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="listings-dashboard-info-card">
|
||||
<span class="listings-dashboard-info-label">Yayına alındı</span>
|
||||
<span class="listings-dashboard-info-label">Published</span>
|
||||
<strong>{{ $publishedLabel }}</strong>
|
||||
<span>İlk görünür olduğu kayıt tarihi</span>
|
||||
<span>First visible date</span>
|
||||
</div>
|
||||
|
||||
<div class="listings-dashboard-info-card">
|
||||
<span class="listings-dashboard-info-label">{{ $listing->expires_at ? 'Bitiş tarihi' : 'Yayın süresi' }}</span>
|
||||
<span class="listings-dashboard-info-label">{{ $listing->expires_at ? 'End date' : 'Listing duration' }}</span>
|
||||
<strong>{{ $expiresLabel }}</strong>
|
||||
<span>{{ $listing->panelExpirySummary() }}</span>
|
||||
</div>
|
||||
|
||||
<div class="listings-dashboard-info-card">
|
||||
<span class="listings-dashboard-info-label">Etkileşim</span>
|
||||
<strong>{{ number_format($viewCount) }} görüntülenme</strong>
|
||||
<span>{{ number_format($favoriteCount) }} favori</span>
|
||||
<span class="listings-dashboard-info-label">Engagement</span>
|
||||
<strong>{{ number_format($viewCount) }} views</strong>
|
||||
<span>{{ number_format($favoriteCount) }} saved</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -226,18 +206,18 @@
|
||||
|
||||
@if ($listing->statusValue() === 'expired')
|
||||
<div class="listings-dashboard-alert is-danger">
|
||||
Bu ilanın süresi doldu. Satıldıysa kapatabilir, devam edecekse yeniden yayına alabilirsin.
|
||||
This listing has expired. If it is sold you can keep it closed, otherwise republish it.
|
||||
</div>
|
||||
@elseif ($listing->statusValue() === 'pending')
|
||||
<div class="listings-dashboard-alert is-warning">
|
||||
İlan şu anda moderasyon kontrolünde. Onaylandığında otomatik olarak yayında görünür.
|
||||
This listing is in moderation review. It will go live automatically once approved.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-4 border-t border-slate-200/80 pt-5 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<a href="{{ route('listings.show', $listing) }}" class="account-secondary-button">İlanı Gör</a>
|
||||
<a href="{{ route('panel.listings.edit', $listing) }}" class="account-primary-button">Düzenle</a>
|
||||
<a href="{{ route('listings.show', $listing) }}" class="account-secondary-button">View Listing</a>
|
||||
<a href="{{ route('panel.listings.edit', $listing) }}" class="account-primary-button">Edit</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
@ -245,14 +225,14 @@
|
||||
<form method="POST" action="{{ route('panel.listings.republish', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="listings-dashboard-text-button">
|
||||
Yeniden Yayınla
|
||||
Republish
|
||||
</button>
|
||||
</form>
|
||||
@elseif ($listing->statusValue() !== 'sold')
|
||||
<form method="POST" action="{{ route('panel.listings.mark-sold', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="listings-dashboard-text-button">
|
||||
Satıldı İşaretle
|
||||
Mark as Sold
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@ -260,7 +240,7 @@
|
||||
<form method="POST" action="{{ route('panel.listings.destroy', $listing) }}">
|
||||
@csrf
|
||||
<button type="submit" class="listings-dashboard-text-button is-danger">
|
||||
İlanı Kaldır
|
||||
Remove Listing
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -274,17 +254,17 @@
|
||||
</article>
|
||||
@empty
|
||||
<div class="listings-dashboard-empty">
|
||||
<p class="account-section-kicker">Boş durum</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">Bu filtreye uygun ilan bulunamadı</h2>
|
||||
<p class="account-section-kicker">Empty State</p>
|
||||
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">No listings match this filter</h2>
|
||||
<p class="mt-3 max-w-xl text-sm leading-6 text-slate-500">
|
||||
Arama terimini temizleyebilir, farklı bir durum seçebilir veya yeni ilan oluşturarak bu alanı doldurabilirsin.
|
||||
Clear the search term, pick another status, or create a new listing to fill this space.
|
||||
</p>
|
||||
<div class="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||
@if ($hasFilters)
|
||||
<a href="{{ route('panel.listings.index') }}" class="account-secondary-button">Filtreleri Temizle</a>
|
||||
<a href="{{ route('panel.listings.index') }}" class="account-secondary-button">Clear Filters</a>
|
||||
@endif
|
||||
|
||||
<a href="{{ route('panel.listings.create') }}" class="account-primary-button">Yeni İlan Ver</a>
|
||||
<a href="{{ route('panel.listings.create') }}" class="account-primary-button">New Listing</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
24
resources/views/panel/partials/page-header.blade.php
Normal file
24
resources/views/panel/partials/page-header.blade.php
Normal file
@ -0,0 +1,24 @@
|
||||
@props([
|
||||
'kicker' => 'Panel',
|
||||
'title',
|
||||
'description' => null,
|
||||
'actions' => null,
|
||||
])
|
||||
|
||||
<div class="panel-surface p-6">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div class="min-w-0">
|
||||
<p class="text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-slate-400">{{ $kicker }}</p>
|
||||
<h1 class="mt-2 text-[2.1rem] font-semibold leading-tight tracking-[-0.04em] text-slate-950">{{ $title }}</h1>
|
||||
@if (filled($description))
|
||||
<p class="mt-3 max-w-3xl text-sm leading-6 text-slate-500">{{ $description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($actions)
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-3">
|
||||
{{ $actions }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@ -49,12 +49,6 @@
|
||||
@endphp
|
||||
|
||||
<aside class="panel-side-nav rounded-[28px] border border-slate-200/80 bg-white/90 p-3 shadow-[0_20px_48px_rgba(15,23,42,0.08)] backdrop-blur">
|
||||
<div class="px-3 pb-3 pt-2">
|
||||
<p class="text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-slate-400">Workspace</p>
|
||||
<p class="mt-2 text-lg font-semibold text-slate-900">Manage your account</p>
|
||||
<p class="mt-1 text-sm leading-6 text-slate-500">Listings, saved items, inbox, and profile settings live here.</p>
|
||||
</div>
|
||||
|
||||
<nav class="space-y-1.5">
|
||||
@foreach ($primaryItems as $item)
|
||||
<a
|
||||
|
||||
@ -8,12 +8,12 @@
|
||||
@include('panel.partials.sidebar', ['activeMenu' => 'videos'])
|
||||
|
||||
<section class="space-y-4">
|
||||
<div class="panel-surface p-6">
|
||||
<div class="flex flex-col gap-1 mb-5">
|
||||
<h1 class="text-2xl font-semibold text-slate-900">Videos</h1>
|
||||
<p class="text-sm text-slate-500">Upload listing videos and manage processing from the frontend panel.</p>
|
||||
</div>
|
||||
@include('panel.partials.page-header', [
|
||||
'title' => 'Videos',
|
||||
'description' => 'Upload listing videos and manage processing from one frontend workspace.',
|
||||
])
|
||||
|
||||
<div class="panel-surface p-6">
|
||||
@if (session('success'))
|
||||
<div class="mb-4 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-700">
|
||||
{{ session('success') }}
|
||||
|
||||
@ -14,13 +14,13 @@ Route::get('/dashboard', fn () => auth()->check()
|
||||
|
||||
Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
|
||||
Route::get('/', [PanelController::class, 'index'])->name('index');
|
||||
Route::get('/ilanlarim', [PanelController::class, 'listings'])->name('listings.index');
|
||||
Route::get('/my-listings', [PanelController::class, 'listings'])->name('listings.index');
|
||||
Route::get('/create-listing', [PanelController::class, 'create'])->name('listings.create');
|
||||
Route::get('/ilanlarim/{listing}/duzenle', [PanelController::class, 'editListing'])->name('listings.edit');
|
||||
Route::put('/ilanlarim/{listing}', [PanelController::class, 'updateListing'])->name('listings.update');
|
||||
Route::post('/ilanlarim/{listing}/kaldir', [PanelController::class, 'destroyListing'])->name('listings.destroy');
|
||||
Route::post('/ilanlarim/{listing}/satildi', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold');
|
||||
Route::post('/ilanlarim/{listing}/yeniden-yayinla', [PanelController::class, 'republishListing'])->name('listings.republish');
|
||||
Route::get('/my-listings/{listing}/edit', [PanelController::class, 'editListing'])->name('listings.edit');
|
||||
Route::put('/my-listings/{listing}', [PanelController::class, 'updateListing'])->name('listings.update');
|
||||
Route::post('/my-listings/{listing}/remove', [PanelController::class, 'destroyListing'])->name('listings.destroy');
|
||||
Route::post('/my-listings/{listing}/mark-sold', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold');
|
||||
Route::post('/my-listings/{listing}/republish', [PanelController::class, 'republishListing'])->name('listings.republish');
|
||||
Route::get('/videos', [PanelController::class, 'videos'])->name('videos.index');
|
||||
Route::post('/videos', [PanelController::class, 'storeVideo'])->name('videos.store');
|
||||
Route::get('/videos/{video}/edit', [PanelController::class, 'editVideo'])->name('videos.edit');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user