Fix listings language and actions

This commit is contained in:
fatihalp 2026-03-08 03:32:25 +03:00
parent 08aad25594
commit f1e2199fef
36 changed files with 1323 additions and 1766 deletions

View File

@ -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 ı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']))

View File

@ -2,6 +2,8 @@
use Illuminate\Support\Facades\Route;
use Modules\Category\Http\Controllers\CategoryController;
Route::prefix('categories')->name('categories.')->group(function () {
Route::get('/', [CategoryController::class, 'index'])->name('index');
Route::middleware('web')->group(function () {
Route::prefix('categories')->name('categories.')->group(function () {
Route::get('/', [CategoryController::class, 'index'])->name('index');
});
});

View File

@ -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();
}
if ($request->expectsJson()) {
return $this->conversationJsonResponse($conversation, $message, (int) $user->getKey());
}
return redirect()
->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()]))
->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.');
->with('success', $messageBody !== '' ? 'Message sent.' : 'Conversation started.');
}
public function send(Request $request, Conversation $conversation): RedirectResponse
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

View File

@ -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',
];
}
}

View File

@ -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>

View File

@ -3,11 +3,13 @@
use Illuminate\Support\Facades\Route;
use Modules\Conversation\App\Http\Controllers\ConversationController;
Route::prefix('panel')->name('panel.')->group(function () {
Route::get('/inbox', [ConversationController::class, 'inbox'])->name('inbox.index');
});
Route::middleware('web')->group(function () {
Route::prefix('panel')->name('panel.')->group(function () {
Route::get('/inbox', [ConversationController::class, 'inbox'])->name('inbox.index');
});
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');
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');
});
});

View File

@ -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();
}
}

View File

@ -51,6 +51,6 @@ class FavoriteSearch extends Model
$labelParts[] = $categoryName;
}
return $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtreli arama';
return $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtered search';
}
}

View File

@ -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ı
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>
<p class="text-xs text-slate-400 mt-1">Active listings: {{ (int) $seller->active_listings_count }}</p>
</div>
</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>

View File

@ -3,13 +3,15 @@
use Illuminate\Support\Facades\Route;
use Modules\Favorite\App\Http\Controllers\FavoriteController;
Route::prefix('favorites')->name('favorites.')->group(function () {
Route::get('/', [FavoriteController::class, 'index'])->name('index');
});
Route::middleware('web')->group(function () {
Route::prefix('favorites')->name('favorites.')->group(function () {
Route::get('/', [FavoriteController::class, 'index'])->name('index');
});
Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
Route::post('/listings/{listing}/toggle', [FavoriteController::class, 'toggleListing'])->name('listings.toggle');
Route::post('/sellers/{seller}/toggle', [FavoriteController::class, 'toggleSeller'])->name('sellers.toggle');
Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store');
Route::delete('/searches/{favoriteSearch}', [FavoriteController::class, 'destroySearch'])->name('searches.destroy');
Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
Route::post('/listings/{listing}/toggle', [FavoriteController::class, 'toggleListing'])->name('listings.toggle');
Route::post('/sellers/{seller}/toggle', [FavoriteController::class, 'toggleSeller'])->name('sellers.toggle');
Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store');
Route::delete('/searches/{favoriteSearch}', [FavoriteController::class, 'destroySearch'])->name('searches.destroy');
});
});

View File

@ -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;

View File

@ -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(

View File

@ -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' : ''),
];
}

View File

@ -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;
}

View File

@ -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

View 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>

View File

@ -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">

View File

@ -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

View File

@ -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">

View File

@ -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

View File

@ -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,49 +372,21 @@
@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
@else
<a href="{{ $loginRedirectRoute }}" class="lt-btn lt-btn-outline">Save seller</a>
@endauth
@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>
<button type="button" class="lt-btn lt-btn-outline" disabled>Your account</button>
@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
<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>
@endauth
@else
<button type="button" class="lt-btn lt-btn-outline" disabled>{{ $isOwnListing ? 'Your account' : 'Save seller' }}</button>
@endif
</div>
</div>
</section>
@ -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
@else
<a href="{{ $loginRedirectRoute }}" class="lt-btn lt-btn-outline">Save seller</a>
@endauth
@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>
<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

View File

@ -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ı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',

View File

@ -24,7 +24,7 @@ class AuthUserSeeder extends Seeder
['email' => 'b@b.com'],
[
'name' => 'Member',
'password' => '36330',
'password' => '236330',
'status' => 'active',
],
);

View File

@ -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

View File

@ -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();

View File

@ -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 {

View File

@ -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) {

View File

@ -1,7 +1 @@
import './bootstrap';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

View File

@ -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>

View File

@ -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>

View File

@ -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([

View File

@ -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

View 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>

View File

@ -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

View File

@ -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') }}

View File

@ -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');