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 return $schema
->components([ ->components([
TextInput::make('site_name') TextInput::make('site_name')
->label('Site Adı') ->label('Site Name')
->default($defaults['site_name']) ->default($defaults['site_name'])
->required() ->required()
->maxLength(255), ->maxLength(255),
Textarea::make('site_description') Textarea::make('site_description')
->label('Site ıklaması') ->label('Site Description')
->default($defaults['site_description']) ->default($defaults['site_description'])
->rows(3) ->rows(3)
->maxLength(500), ->maxLength(500),
Select::make('media_disk') Select::make('media_disk')
->label('Medya Depolama') ->label('Media Storage')
->options(MediaStorage::options()) ->options(MediaStorage::options())
->default($defaults['media_disk']) ->default($defaults['media_disk'])
->required() ->required()
->native(false) ->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( HomeSlideFormSchema::make(
$defaults['home_slides'], $defaults['home_slides'],
fn ($state): array => $this->normalizeHomeSlides($state, MediaStorage::activeDisk()), fn ($state): array => $this->normalizeHomeSlides($state, MediaStorage::activeDisk()),
), ),
Hidden::make('site_logo_disk'), Hidden::make('site_logo_disk'),
FileUpload::make('site_logo') FileUpload::make('site_logo')
->label('Site Logosu') ->label('Site Logo')
->image() ->image()
->disk(fn (Get $get): string => MediaStorage::storedDisk($get('site_logo_disk'), $get('media_disk'))) ->disk(fn (Get $get): string => MediaStorage::storedDisk($get('site_logo_disk'), $get('media_disk')))
->directory('settings') ->directory('settings')
@ -133,32 +133,32 @@ class ManageGeneralSettings extends SettingsPage
); );
}), }),
TextInput::make('sender_name') TextInput::make('sender_name')
->label('Gönderici Adı') ->label('Sender Name')
->default($defaults['sender_name']) ->default($defaults['sender_name'])
->required() ->required()
->maxLength(120), ->maxLength(120),
TextInput::make('sender_email') TextInput::make('sender_email')
->label('Gönderici E-postası') ->label('Sender Email')
->email() ->email()
->default($defaults['sender_email']) ->default($defaults['sender_email'])
->required() ->required()
->maxLength(255), ->maxLength(255),
Select::make('default_language') Select::make('default_language')
->label('Varsayılan Dil') ->label('Default Language')
->options($this->localeOptions()) ->options($this->localeOptions())
->default($defaults['default_language']) ->default($defaults['default_language'])
->required() ->required()
->searchable(), ->searchable(),
CountryCodeSelect::make('default_country_code') CountryCodeSelect::make('default_country_code')
->label('Varsayılan Ülke') ->label('Default Country')
->default($defaults['default_country_code']) ->default($defaults['default_country_code'])
->required() ->required()
->helperText('Panel formlarında varsayılan ülke olarak kullanılır.'), ->helperText('Used as the default country in panel forms.'),
TagsInput::make('currencies') TagsInput::make('currencies')
->label('Para Birimleri') ->label('Currencies')
->placeholder('TRY') ->placeholder('TRY')
->default($defaults['currencies']) ->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() ->required()
->rules(['array', 'min:1']) ->rules(['array', 'min:1'])
->afterStateHydrated(fn (TagsInput $component, $state) => $component->state($this->normalizeCurrencies($state))) ->afterStateHydrated(fn (TagsInput $component, $state) => $component->state($this->normalizeCurrencies($state)))
@ -181,19 +181,19 @@ class ManageGeneralSettings extends SettingsPage
->default($defaults['whatsapp']) ->default($defaults['whatsapp'])
->nullable() ->nullable()
->formatAsYouType() ->formatAsYouType()
->helperText('Uluslararası format kullanın. Örnek: +905551112233'), ->helperText('Use international format. Example: +905551112233'),
Toggle::make('enable_google_maps') Toggle::make('enable_google_maps')
->label('Google Maps Aktif') ->label('Google Maps Enabled')
->default($defaults['enable_google_maps']), ->default($defaults['enable_google_maps']),
TextInput::make('google_maps_api_key') TextInput::make('google_maps_api_key')
->label('Google Maps API Anahtarı') ->label('Google Maps API Key')
->password() ->password()
->revealable() ->revealable()
->nullable() ->nullable()
->maxLength(255) ->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') Toggle::make('enable_google_login')
->label('Google ile Giriş Aktif') ->label('Google Login Enabled')
->default($defaults['enable_google_login']), ->default($defaults['enable_google_login']),
TextInput::make('google_client_id') TextInput::make('google_client_id')
->label('Google Client ID') ->label('Google Client ID')
@ -206,7 +206,7 @@ class ManageGeneralSettings extends SettingsPage
->nullable() ->nullable()
->maxLength(255), ->maxLength(255),
Toggle::make('enable_facebook_login') Toggle::make('enable_facebook_login')
->label('Facebook ile Giriş Aktif') ->label('Facebook Login Enabled')
->default($defaults['enable_facebook_login']), ->default($defaults['enable_facebook_login']),
TextInput::make('facebook_client_id') TextInput::make('facebook_client_id')
->label('Facebook Client ID') ->label('Facebook Client ID')
@ -219,7 +219,7 @@ class ManageGeneralSettings extends SettingsPage
->nullable() ->nullable()
->maxLength(255), ->maxLength(255),
Toggle::make('enable_apple_login') Toggle::make('enable_apple_login')
->label('Apple ile Giriş Aktif') ->label('Apple Login Enabled')
->default($defaults['enable_apple_login']), ->default($defaults['enable_apple_login']),
TextInput::make('apple_client_id') TextInput::make('apple_client_id')
->label('Apple Client ID') ->label('Apple Client ID')
@ -241,13 +241,13 @@ class ManageGeneralSettings extends SettingsPage
return [ return [
'site_name' => $siteName, '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(), 'media_disk' => MediaStorage::defaultDriver(),
'home_slides' => $this->defaultHomeSlides(), 'home_slides' => $this->defaultHomeSlides(),
'site_logo_disk' => null, 'site_logo_disk' => null,
'sender_name' => $siteName, 'sender_name' => $siteName,
'sender_email' => (string) config('mail.from.address', 'info@' . $siteHost), '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')), 'default_country_code' => CountryCodeManager::normalizeCountryCode(config('app.default_country_code', '+90')),
'currencies' => $this->normalizeCurrencies(config('app.currencies', ['TRY'])), 'currencies' => $this->normalizeCurrencies(config('app.currencies', ['TRY'])),
'linkedin_url' => 'https://www.linkedin.com/company/openclassify', 'linkedin_url' => 'https://www.linkedin.com/company/openclassify',
@ -264,7 +264,7 @@ class ManageGeneralSettings extends SettingsPage
{ {
$labels = [ $labels = [
'en' => 'English', 'en' => 'English',
'tr' => 'Türkçe', 'tr' => 'Turkish',
]; ];
return collect(config('app.available_locales', ['en'])) return collect(config('app.available_locales', ['en']))

View File

@ -2,6 +2,8 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Modules\Category\Http\Controllers\CategoryController; use Modules\Category\Http\Controllers\CategoryController;
Route::prefix('categories')->name('categories.')->group(function () { Route::middleware('web')->group(function () {
Route::get('/', [CategoryController::class, 'index'])->name('index'); 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; namespace Modules\Conversation\App\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\View\View; use Illuminate\View\View;
use Modules\Conversation\App\Models\Conversation; use Modules\Conversation\App\Models\Conversation;
use Modules\Conversation\App\Models\ConversationMessage;
use Modules\Conversation\App\Support\QuickMessageCatalog; use Modules\Conversation\App\Support\QuickMessageCatalog;
use Modules\Listing\Models\Listing; use Modules\Listing\Models\Listing;
use Throwable; 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()) { 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(); $user = $request->user();
if (! $listing->user_id) { 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()) { 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()); $conversation = Conversation::openForListingBuyer($listing, (int) $user->getKey());
$user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]); $user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]);
$messageBody = trim((string) $request->string('message')); $message = null;
if ($messageBody !== '') { if ($messageBody !== '') {
$message = $conversation->messages()->create([ $message = $conversation->messages()->create([
'sender_id' => $user->getKey(), 'sender_id' => $user->getKey(),
@ -87,15 +106,23 @@ class ConversationController extends Controller
$conversation->forceFill(['last_message_at' => $message->created_at])->save(); $conversation->forceFill(['last_message_at' => $message->created_at])->save();
} }
if ($request->expectsJson()) {
return $this->conversationJsonResponse($conversation, $message, (int) $user->getKey());
}
return redirect() return redirect()
->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()])) ->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()) { 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(); $user = $request->user();
@ -112,7 +139,11 @@ class ConversationController extends Controller
$messageBody = trim($payload['message']); $messageBody = trim($payload['message']);
if ($messageBody === '') { 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([ $message = $conversation->messages()->create([
@ -122,9 +153,32 @@ class ConversationController extends Controller
$conversation->forceFill(['last_message_at' => $message->created_at])->save(); $conversation->forceFill(['last_message_at' => $message->created_at])->save();
if ($request->expectsJson()) {
return $this->conversationJsonResponse($conversation, $message, $userId);
}
return redirect() return redirect()
->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()])) ->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 private function inboxFilters(Request $request): array

View File

@ -7,10 +7,10 @@ class QuickMessageCatalog
public static function all(): array public static function all(): array
{ {
return [ return [
'Merhaba', 'Hi',
'İlan hâlâ satışta mı?', 'Is this listing still available?',
'Son fiyat nedir?', 'What is your best price?',
'Teşekkürler', 'Thanks',
]; ];
} }
} }

View File

@ -1,44 +1,44 @@
@extends('app::layouts.app') @extends('app::layouts.app')
@section('title', 'Gelen Kutusu') @section('title', 'Inbox')
@section('content') @section('content')
<div class="max-w-[1320px] mx-auto px-4 py-8"> <div class="max-w-[1320px] mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4"> <div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
@include('panel.partials.sidebar', ['activeMenu' => 'inbox']) @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) @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> <div>
<h1 class="text-xl font-semibold text-slate-900">Inbox</h1> <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> </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> </div>
@endif @endif
<div class="grid grid-cols-1 xl:grid-cols-[420px,1fr] min-h-[620px]"> <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="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"> <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"> <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' }}"> <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>
<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' }}"> <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>
<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' }}"> <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> </a>
</div> </div>
</div> </div>
@ -57,17 +57,17 @@
@if($conversationImage) @if($conversationImage)
<img src="{{ $conversationImage }}" alt="{{ $conversationListing?->title }}" class="w-full h-full object-cover"> <img src="{{ $conversationImage }}" alt="{{ $conversationListing?->title }}" class="w-full h-full object-cover">
@else @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 @endif
</div> </div>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-start gap-2"> <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> <p class="text-xs text-slate-500 whitespace-nowrap ml-auto">{{ $conversation->last_message_at?->format('d.m.Y') }}</p>
</div> </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"> <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> </p>
</div> </div>
@if($conversation->unread_count > 0) @if($conversation->unread_count > 0)
@ -79,7 +79,7 @@
</a> </a>
@empty @empty
<div class="px-6 py-16 text-center text-slate-500"> <div class="px-6 py-16 text-center text-slate-500">
Henüz bir sohbetin yok. No conversations yet.
</div> </div>
@endforelse @endforelse
</div> </div>
@ -101,8 +101,8 @@
{{ strtoupper(substr((string) ($activePartner?->name ?? 'K'), 0, 1)) }} {{ strtoupper(substr((string) ($activePartner?->name ?? 'K'), 0, 1)) }}
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<p class="text-3xl font-bold text-slate-900 truncate">{{ $activePartner?->name ?? 'Kullanıcı' }}</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 ?? 'İlan silinmiş' }}</p> <p class="text-sm text-slate-500 truncate">{{ $activeListing?->title ?? 'Listing removed' }}</p>
</div> </div>
@if($activePriceLabel) @if($activePriceLabel)
<div class="ml-auto text-3xl font-semibold text-slate-800 whitespace-nowrap">{{ $activePriceLabel }}</div> <div class="ml-auto text-3xl font-semibold text-slate-800 whitespace-nowrap">{{ $activePriceLabel }}</div>
@ -125,8 +125,8 @@
@empty @empty
<div class="h-full grid place-items-center text-slate-500 text-center px-8"> <div class="h-full grid place-items-center text-slate-500 text-center px-8">
<div> <div>
<p class="font-semibold text-slate-700">Henüz mesaj yok.</p> <p class="font-semibold text-slate-700">No messages yet.</p>
<p class="text-sm mt-1">Aşağıdaki hazır metinlerden birini seçebilir veya yeni mesaj yazabilirsin.</p> <p class="mt-1 text-sm">Use a quick reply or send the first message below.</p>
</div> </div>
</div> </div>
@endforelse @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"> <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 @csrf
<input type="hidden" name="message_filter" value="{{ $messageFilter }}"> <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> <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="Gönder"> <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"> <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"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h13m0 0l-5-5m5 5l-5 5"/>
</svg> </svg>
@ -162,13 +162,14 @@
@else @else
<div class="h-full min-h-[620px] grid place-items-center px-8 text-center text-slate-500"> <div class="h-full min-h-[620px] grid place-items-center px-8 text-center text-slate-500">
<div> <div>
<p class="text-2xl font-semibold text-slate-700">Mesajlaşma için bir sohbet seç.</p> <p class="text-2xl font-semibold text-slate-700">Choose a conversation to start messaging.</p>
<p class="mt-2 text-sm">İlan detayından veya ilan kartlarından yeni sohbet başlatabilirsin.</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>
</div> </div>
@endif @endif
</div> </div>
</div> </div>
</div>
</section> </section>
</div> </div>
</div> </div>

View File

@ -3,11 +3,13 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Modules\Conversation\App\Http\Controllers\ConversationController; use Modules\Conversation\App\Http\Controllers\ConversationController;
Route::prefix('panel')->name('panel.')->group(function () { Route::middleware('web')->group(function () {
Route::get('/inbox', [ConversationController::class, 'inbox'])->name('inbox.index'); Route::prefix('panel')->name('panel.')->group(function () {
}); Route::get('/inbox', [ConversationController::class, 'inbox'])->name('inbox.index');
});
Route::middleware('auth')->name('conversations.')->group(function () { Route::middleware('auth')->name('conversations.')->group(function () {
Route::post('/listings/{listing}/conversation', [ConversationController::class, 'start'])->name('start'); Route::post('/listings/{listing}/conversation', [ConversationController::class, 'start'])->name('start');
Route::post('/conversations/{conversation}/messages', [ConversationController::class, 'send'])->name('messages.send'); 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\Favorite\App\Models\FavoriteSearch;
use Modules\Listing\Models\Listing; use Modules\Listing\Models\Listing;
use Modules\User\App\Models\User; use Modules\User\App\Models\User;
use Modules\User\App\Support\AuthRedirector;
use Throwable; use Throwable;
class FavoriteController extends Controller class FavoriteController extends Controller
{ {
public function __construct(private AuthRedirector $redirector)
{
}
public function index(Request $request) public function index(Request $request)
{ {
$activeTab = (string) $request->string('tab', 'listings'); $activeTab = (string) $request->string('tab', 'listings');
@ -126,7 +131,7 @@ class FavoriteController extends Controller
{ {
$isNowFavorite = $request->user()->toggleFavoriteListing($listing); $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) public function toggleSeller(Request $request, User $seller)
@ -134,12 +139,12 @@ class FavoriteController extends Controller
$user = $request->user(); $user = $request->user();
if ((int) $user->getKey() === (int) $seller->getKey()) { 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); $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) public function storeSearch(Request $request)
@ -155,7 +160,7 @@ class FavoriteController extends Controller
]); ]);
if ($filters === []) { 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); $signature = FavoriteSearch::signatureFor($filters);
@ -178,10 +183,10 @@ class FavoriteController extends Controller
); );
if (! $favoriteSearch->wasRecentlyCreated) { 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) public function destroySearch(Request $request, FavoriteSearch $favoriteSearch)
@ -192,7 +197,7 @@ class FavoriteController extends Controller
$favoriteSearch->delete(); $favoriteSearch->delete();
return back()->with('success', 'Favori arama silindi.'); return back()->with('success', 'Saved search deleted.');
} }
private function tableExists(string $table): bool private function tableExists(string $table): bool
@ -211,4 +216,15 @@ class FavoriteController extends Controller
'query' => request()->query(), '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; $labelParts[] = $categoryName;
} }
return $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtreli arama'; return $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtered search';
} }
} }

View File

@ -1,6 +1,6 @@
@extends('app::layouts.app') @extends('app::layouts.app')
@section('title', 'Favoriler') @section('title', 'Favorites')
@section('content') @section('content')
<div class="max-w-[1320px] mx-auto px-4 py-8"> <div class="max-w-[1320px] mx-auto px-4 py-8">
@ -29,25 +29,25 @@
], fn ($value) => !is_null($value) && $value !== ''); ], fn ($value) => !is_null($value) && $value !== '');
@endphp @endphp
<div class="border-b-2 border-blue-900 px-4 py-3 flex flex-wrap items-center gap-3"> <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"> <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' }}"> <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>
<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' }}"> <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> </a>
</div> </div>
<form method="GET" action="{{ route('favorites.index') }}" class="flex items-center gap-2"> <form method="GET" action="{{ route('favorites.index') }}" class="flex items-center gap-2">
<input type="hidden" name="tab" value="listings"> <input type="hidden" name="tab" value="listings">
<input type="hidden" name="status" value="{{ $statusFilter }}"> <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"> <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) @foreach($categories as $category)
<option value="{{ $category->id }}" @selected((int) $selectedCategoryId === (int) $category->id)>{{ $category->name }}</option> <option value="{{ $category->id }}" @selected((int) $selectedCategoryId === (int) $category->id)>{{ $category->name }}</option>
@endforeach @endforeach
</select> </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> </form>
</div> </div>
@ -55,9 +55,9 @@
<table class="w-full min-w-[860px]"> <table class="w-full min-w-[860px]">
<thead> <thead>
<tr class="bg-slate-50 text-slate-700 text-sm"> <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-[58%]">Listing</th>
<th class="text-left px-4 py-3 w-[16%]">Fiyat</th> <th class="text-left px-4 py-3 w-[16%]">Price</th>
<th class="text-left px-4 py-3 w-[14%]">Mesajlaşma</th> <th class="text-left px-4 py-3 w-[14%]">Messaging</th>
<th class="text-right px-4 py-3 w-[12%]"></th> <th class="text-right px-4 py-3 w-[12%]"></th>
</tr> </tr>
</thead> </thead>
@ -65,7 +65,7 @@
@forelse($favoriteListings as $listing) @forelse($favoriteListings as $listing)
@php @php
$listingImage = $listing->getFirstMediaUrl('listing-images'); $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([ $meta = collect([
$listing->category?->name, $listing->category?->name,
$listing->city, $listing->city,
@ -82,15 +82,15 @@
@if($listingImage) @if($listingImage)
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover"> <img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover">
@else @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 @endif
</a> </a>
<div> <div>
<a href="{{ route('listings.show', $listing) }}" class="font-semibold text-2xl text-slate-800 hover:text-blue-700 transition leading-6"> <a href="{{ route('listings.show', $listing) }}" class="font-semibold text-2xl text-slate-800 hover:text-blue-700 transition leading-6">
{{ $listing->title }} {{ $listing->title }}
</a> </a>
<p class="text-sm text-slate-500 mt-2">{{ $meta !== '' ? $meta : 'Kategori / konum bilgisi yok' }}</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">Favoriye eklenme: {{ $listing->pivot->created_at?->format('d.m.Y') }}</p> <p class="text-xs text-slate-400 mt-1">Saved on: {{ $listing->pivot->created_at?->format('M j, Y') }}</p>
</div> </div>
</div> </div>
</td> </td>
@ -99,31 +99,31 @@
@if($canMessageListing) @if($canMessageListing)
@if($conversationId) @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"> <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> </a>
@else @else
<form method="POST" action="{{ route('conversations.start', $listing) }}"> <form method="POST" action="{{ route('conversations.start', $listing) }}">
@csrf @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"> <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> </button>
</form> </form>
@endif @endif
@else @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 @endif
</td> </td>
<td class="px-4 py-4 text-right"> <td class="px-4 py-4 text-right">
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}"> <form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
@csrf @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> </form>
</td> </td>
</tr> </tr>
@empty @empty
<tr class="border-t border-slate-200"> <tr class="border-t border-slate-200">
<td colspan="4" class="px-4 py-10 text-center text-slate-500"> <td colspan="4" class="px-4 py-10 text-center text-slate-500">
Henüz favori ilan bulunmuyor. No saved listings yet.
</td> </td>
</tr> </tr>
@endforelse @endforelse
@ -132,7 +132,7 @@
</div> </div>
<div class="px-4 py-4 border-t border-slate-200 text-sm text-slate-500"> <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> </div>
@if($favoriteListings?->hasPages()) @if($favoriteListings?->hasPages())
@ -142,8 +142,8 @@
@if($activeTab === 'searches') @if($activeTab === 'searches')
<div class="px-4 py-4 border-b border-slate-200"> <div class="px-4 py-4 border-b border-slate-200">
<h1 class="text-3xl font-bold text-slate-800">Favori Aramalar</h1> <h1 class="text-3xl font-bold text-slate-800">Saved Searches</h1>
<p class="text-sm text-slate-500 mt-1">Kayıtlı aramalarına tek tıkla geri dön.</p> <p class="text-sm text-slate-500 mt-1">Return to your saved searches with one click.</p>
</div> </div>
<div class="divide-y divide-slate-200"> <div class="divide-y divide-slate-200">
@forelse($favoriteSearches as $favoriteSearch) @forelse($favoriteSearches as $favoriteSearch)
@ -155,29 +155,29 @@
@endphp @endphp
<article class="px-4 py-4 flex flex-col md:flex-row md:items-center gap-3"> <article class="px-4 py-4 flex flex-col md:flex-row md:items-center gap-3">
<div class="flex-1"> <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"> <p class="text-sm text-slate-500 mt-1">
@if($favoriteSearch->search_term) Arama: "{{ $favoriteSearch->search_term }}" · @endif @if($favoriteSearch->search_term) Search: "{{ $favoriteSearch->search_term }}" · @endif
@if($favoriteSearch->category) Kategori: {{ $favoriteSearch->category->name }} · @endif @if($favoriteSearch->category) Category: {{ $favoriteSearch->category->name }} · @endif
Kaydedilme: {{ $favoriteSearch->created_at?->format('d.m.Y H:i') }} Saved: {{ $favoriteSearch->created_at?->format('M j, Y H:i') }}
</p> </p>
</div> </div>
<div class="flex items-center gap-3"> <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"> <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> </a>
<form method="POST" action="{{ route('favorites.searches.destroy', $favoriteSearch) }}"> <form method="POST" action="{{ route('favorites.searches.destroy', $favoriteSearch) }}">
@csrf @csrf
@method('DELETE') @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"> <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> </button>
</form> </form>
</div> </div>
</article> </article>
@empty @empty
<div class="px-4 py-10 text-center text-slate-500"> <div class="px-4 py-10 text-center text-slate-500">
Henüz favori arama eklenmedi. No saved searches yet.
</div> </div>
@endforelse @endforelse
</div> </div>
@ -188,32 +188,32 @@
@if($activeTab === 'sellers') @if($activeTab === 'sellers')
<div class="px-4 py-4 border-b border-slate-200"> <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> <h1 class="text-3xl font-bold text-slate-800">Saved Sellers</h1>
<p class="text-sm text-slate-500 mt-1">Takip etmek istediğin satıcıları burada yönetebilirsin.</p> <p class="text-sm text-slate-500 mt-1">Manage the sellers you want to follow here.</p>
</div> </div>
<div class="divide-y divide-slate-200"> <div class="divide-y divide-slate-200">
@forelse($favoriteSellers as $seller) @forelse($favoriteSellers as $seller)
<article class="px-4 py-4 flex flex-col md:flex-row md:items-center gap-3"> <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"> <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)) }} {{ strtoupper(substr((string) $seller->name, 0, 1)) }}
</div> </div>
<div> <div>
<h2 class="font-semibold text-slate-800">{{ $seller->name }}</h2> <h2 class="font-semibold text-slate-800">{{ $seller->name }}</h2>
<p class="text-sm text-slate-500">{{ $seller->email }}</p> <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>
</div> </a>
<form method="POST" action="{{ route('favorites.sellers.toggle', $seller) }}"> <form method="POST" action="{{ route('favorites.sellers.toggle', $seller) }}">
@csrf @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"> <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> </button>
</form> </form>
</article> </article>
@empty @empty
<div class="px-4 py-10 text-center text-slate-500"> <div class="px-4 py-10 text-center text-slate-500">
Henüz favori satıcı eklenmedi. No saved sellers yet.
</div> </div>
@endforelse @endforelse
</div> </div>

View File

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

View File

@ -25,13 +25,13 @@ class ListingSeeder extends Seeder
]; ];
private const TITLE_PREFIXES = [ private const TITLE_PREFIXES = [
'Temiz kullanılmış', 'Clean',
'Az kullanılmış', 'Lightly used',
'Fırsat ürün', 'Special offer',
'Uygun fiyatlı', 'Well priced',
'Sahibinden', 'Owner listed',
'Kaçırılmayacak', 'Must-see',
'Bakımlı', 'Well kept',
]; ];
public function run(): void public function run(): void
@ -100,7 +100,7 @@ class ListingSeeder extends Seeder
private function resolveTurkeyCities(): Collection private function resolveTurkeyCities(): Collection
{ {
if (! class_exists(City::class) || ! Schema::hasTable('cities') || ! Schema::hasTable('countries')) { 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() $turkey = Country::query()
@ -108,7 +108,7 @@ class ListingSeeder extends Seeder
->first(['id']); ->first(['id']);
if (! $turkey) { if (! $turkey) {
return collect(['İstanbul', 'Ankara', zmir', 'Bursa', 'Antalya']); return collect(['Istanbul', 'Ankara', 'Izmir', 'Bursa', 'Antalya']);
} }
$cities = City::query() $cities = City::query()
@ -122,7 +122,7 @@ class ListingSeeder extends Seeder
return $cities->isNotEmpty() return $cities->isNotEmpty()
? $cities ? $cities
: collect(['İstanbul', 'Ankara', zmir', 'Bursa', 'Antalya']); : collect(['Istanbul', 'Ankara', 'Izmir', 'Bursa', 'Antalya']);
} }
private function buildListingData( private function buildListingData(
@ -147,7 +147,7 @@ class ListingSeeder extends Seeder
private function resolveLocation(int $index, Collection $countries, Collection $turkeyCities): array private function resolveLocation(int $index, Collection $countries, Collection $turkeyCities): array
{ {
$turkeyCountry = $countries->first(fn ($country): bool => strtoupper((string) $country->code) === 'TR'); $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; $useForeignCountry = $countries->count() > 1 && $index % 4 === 0;
@ -161,8 +161,8 @@ class ListingSeeder extends Seeder
$countryName = trim((string) ($selected->name ?? '')); $countryName = trim((string) ($selected->name ?? ''));
return [ return [
'country' => $countryName !== '' ? $countryName : 'Türkiye', 'country' => $countryName !== '' ? $countryName : 'Turkey',
'city' => $countryName !== '' ? $countryName : 'İstanbul', 'city' => $countryName !== '' ? $countryName : 'Istanbul',
]; ];
} }
} }
@ -171,7 +171,7 @@ class ListingSeeder extends Seeder
return [ return [
'country' => $turkeyName, '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)]; $prefix = self::TITLE_PREFIXES[$index % count(self::TITLE_PREFIXES)];
$categoryName = trim((string) $category->name); $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 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(', ')); $location = trim(collect([$city, $country])->filter()->join(', '));
return sprintf( return sprintf(
'%s kategorisinde, durum olarak sorunsuz ve kullanıma hazırdır. Teslimat noktası: %s. Detaylar için mesaj atabilirsiniz.', 'Listed in %s, in clean condition and ready to use. Pickup area: %s. Message for more details.',
$categoryName !== '' ? $categoryName : 'Ürün', $categoryName !== '' ? $categoryName : 'Item',
$location !== '' ? $location : 'Türkiye' $location !== '' ? $location : 'Turkey'
); );
} }
@ -244,7 +244,7 @@ class ListingSeeder extends Seeder
if (! is_file($imageAbsolutePath)) { if (! is_file($imageAbsolutePath)) {
if ($this->command) { if ($this->command) {
$this->command->warn("Gorsel bulunamadi: {$imageRelativePath}"); $this->command->warn("Image not found: {$imageRelativePath}");
} }
return; return;

View File

@ -33,6 +33,9 @@ class ListingController extends Controller
$cityId = request()->integer('city'); $cityId = request()->integer('city');
$cityId = $cityId > 0 ? $cityId : null; $cityId = $cityId > 0 ? $cityId : null;
$sellerUserId = request()->integer('user');
$sellerUserId = $sellerUserId > 0 ? $sellerUserId : null;
$minPriceInput = trim((string) request('min_price', '')); $minPriceInput = trim((string) request('min_price', ''));
$maxPriceInput = trim((string) request('max_price', '')); $maxPriceInput = trim((string) request('max_price', ''));
$minPrice = is_numeric($minPriceInput) ? max((float) $minPriceInput, 0) : null; $minPrice = is_numeric($minPriceInput) ? max((float) $minPriceInput, 0) : null;
@ -70,6 +73,7 @@ class ListingController extends Controller
'search' => $search, 'search' => $search,
'country' => $selectedCountryName, 'country' => $selectedCountryName,
'city' => $selectedCityName, 'city' => $selectedCityName,
'user_id' => $sellerUserId,
'min_price' => $minPrice, 'min_price' => $minPrice,
'max_price' => $maxPrice, 'max_price' => $maxPrice,
'date_filter' => $dateFilter, 'date_filter' => $dateFilter,
@ -136,6 +140,7 @@ class ListingController extends Controller
'categoryId', 'categoryId',
'countryId', 'countryId',
'cityId', 'cityId',
'sellerUserId',
'minPriceInput', 'minPriceInput',
'maxPriceInput', 'maxPriceInput',
'dateFilter', 'dateFilter',
@ -184,6 +189,7 @@ class ListingController extends Controller
$isListingFavorited = false; $isListingFavorited = false;
$isSellerFavorited = false; $isSellerFavorited = false;
$existingConversationId = null; $existingConversationId = null;
$detailConversation = null;
if (auth()->check()) { if (auth()->check()) {
$userId = (int) auth()->id(); $userId = (int) auth()->id();
@ -205,6 +211,17 @@ class ListingController extends Controller
(int) $listing->getKey(), (int) $listing->getKey(),
$userId, $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', 'isSellerFavorited',
'presentableCustomFields', 'presentableCustomFields',
'existingConversationId', 'existingConversationId',
'detailConversation',
'gallery', 'gallery',
'listingVideos', 'listingVideos',
'relatedListings', 'relatedListings',
@ -239,7 +257,7 @@ class ListingController extends Controller
return redirect() return redirect()
->route('panel.listings.create') ->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( private function resolveLocationFilters(

View File

@ -146,6 +146,7 @@ class Listing extends Model implements HasMedia
$search = trim((string) ($filters['search'] ?? '')); $search = trim((string) ($filters['search'] ?? ''));
$country = isset($filters['country']) ? trim((string) $filters['country']) : null; $country = isset($filters['country']) ? trim((string) $filters['country']) : null;
$city = isset($filters['city']) ? trim((string) $filters['city']) : 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; $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; $maxPrice = is_numeric($filters['max_price'] ?? null) ? max((float) $filters['max_price'], 0) : null;
$dateFilter = (string) ($filters['date_filter'] ?? 'all'); $dateFilter = (string) ($filters['date_filter'] ?? 'all');
@ -154,6 +155,7 @@ class Listing extends Model implements HasMedia
$query $query
->searchTerm($search) ->searchTerm($search)
->forCategoryIds(is_array($categoryIds) ? $categoryIds : null) ->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($country !== null && $country !== '', fn (Builder $builder) => $builder->where('country', $country))
->when($city !== null && $city !== '', fn (Builder $builder) => $builder->where('city', $city)) ->when($city !== null && $city !== '', fn (Builder $builder) => $builder->where('city', $city))
->when(! is_null($minPrice), fn (Builder $builder) => $builder->whereNotNull('price')->where('price', '>=', $minPrice)) ->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 public function panelPriceLabel(): string
{ {
if (is_null($this->price)) { if (is_null($this->price)) {
return 'Ücretsiz'; return 'Free';
} }
return number_format((float) $this->price, 2, ',', '.').' '.($this->currency ?? 'TL'); return number_format((float) $this->price, 2, ',', '.').' '.($this->currency ?? 'TL');
@ -269,24 +271,24 @@ class Listing extends Model implements HasMedia
{ {
return match ($this->statusValue()) { return match ($this->statusValue()) {
'sold' => [ 'sold' => [
'label' => 'Satıldı', 'label' => 'Sold',
'badge_class' => 'is-success', 'badge_class' => 'is-success',
'hint' => 'İlan satıldı olarak işaretlendi.', 'hint' => 'This listing is marked as sold.',
], ],
'expired' => [ 'expired' => [
'label' => 'Süresi doldu', 'label' => 'Expired',
'badge_class' => 'is-danger', 'badge_class' => 'is-danger',
'hint' => 'Yeniden yayına alınmayı bekliyor.', 'hint' => 'This listing is waiting to be republished.',
], ],
'pending' => [ 'pending' => [
'label' => 'İncelemede', 'label' => 'Pending review',
'badge_class' => 'is-warning', 'badge_class' => 'is-warning',
'hint' => 'Moderasyon onayı bekleniyor.', 'hint' => 'Waiting for moderation approval.',
], ],
default => [ default => [
'label' => 'Yayında', 'label' => 'Live',
'badge_class' => 'is-primary', '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), trim((string) $this->country),
])->filter()->values(); ])->filter()->values();
return $parts->isNotEmpty() ? $parts->implode(', ') : 'Konum belirtilmedi'; return $parts->isNotEmpty() ? $parts->implode(', ') : 'Location not specified';
} }
public function panelPublishedAt(): ?Carbon public function panelPublishedAt(): ?Carbon
@ -320,16 +322,16 @@ class Listing extends Model implements HasMedia
public function panelExpirySummary(): string public function panelExpirySummary(): string
{ {
if (! $this->expires_at) { if (! $this->expires_at) {
return 'Süre sınırı yok'; return 'No expiry limit';
} }
$expiresAt = $this->expires_at->copy()->startOfDay(); $expiresAt = $this->expires_at->copy()->startOfDay();
$days = Carbon::today()->diffInDays($expiresAt, false); $days = Carbon::today()->diffInDays($expiresAt, false);
return match (true) { return match (true) {
$days > 0 => $days.' gün kaldı', $days > 0 => $days.' days left',
$days === 0 => 'Bugün sona eriyor', $days === 0 => 'Ends today',
default => abs($days).' gün önce sona erdi', default => 'Expired '.abs($days).' days ago',
}; };
} }
@ -340,8 +342,8 @@ class Listing extends Model implements HasMedia
} }
return [ return [
'label' => $total.' video', 'label' => $total.' videos',
'detail' => $ready.' hazır'.($pending > 0 ? ', '.$pending.' işleniyor' : ''), 'detail' => $ready.' ready'.($pending > 0 ? ', '.$pending.' processing' : ''),
]; ];
} }

View File

@ -66,12 +66,12 @@ class ListingCustomFieldSchemaBuilder
$label = $field?->label ?: Str::headline((string) $key); $label = $field?->label ?: Str::headline((string) $key);
if (is_bool($value)) { if (is_bool($value)) {
$displayValue = $value ? 'Evet' : 'Hayır'; $displayValue = $value ? 'Yes' : 'No';
} elseif (is_array($value)) { } elseif (is_array($value)) {
$displayValue = implode(', ', array_map(fn ($item): string => (string) $item, $value)); $displayValue = implode(', ', array_map(fn ($item): string => (string) $item, $value));
} elseif ($field?->type === ListingCustomField::TYPE_DATE) { } elseif ($field?->type === ListingCustomField::TYPE_DATE) {
try { try {
$displayValue = Carbon::parse((string) $value)->format('d.m.Y'); $displayValue = Carbon::parse((string) $value)->format('M j, Y');
} catch (\Throwable) { } catch (\Throwable) {
$displayValue = (string) $value; $displayValue = (string) $value;
} }

View File

@ -1,450 +1,7 @@
@extends('app::layouts.app') @extends('app::layouts.app')
@section('title', trim((string) ($selectedCategory?->name ?? '')) !== '' ? trim((string) $selectedCategory->name).' Listings and Prices' : 'All Listings and Prices')
@section('content') @section('content')
@php @include('listing::partials.index-content')
$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>
@endsection @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) }}"> <form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
@csrf @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' }}"> <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> </button>
</form> </form>
@if($listing->user && (int) $listing->user->id !== (int) auth()->id()) @if($listing->user && (int) $listing->user->id !== (int) auth()->id())
<form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}"> <form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}">
@csrf @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' }}"> <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> </button>
</form> </form>
@if($existingConversationId) @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"> <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> </a>
@else @else
<form method="POST" action="{{ route('conversations.start', $listing) }}"> <form method="POST" action="{{ route('conversations.start', $listing) }}">
@csrf @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"> <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> </button>
</form> </form>
@endif @endif
@endif @endif
@else @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"> <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> </a>
@endauth @endauth
</div> </div>
@ -79,7 +79,7 @@
</div> </div>
@if(($presentableCustomFields ?? []) !== []) @if(($presentableCustomFields ?? []) !== [])
<div class="mt-6 border-t pt-4"> <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"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
@foreach($presentableCustomFields as $field) @foreach($presentableCustomFields as $field)
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2"> <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') @extends('app::layouts.app')
@section('title', trim((string) ($selectedCategory?->name ?? '')) !== '' ? trim((string) $selectedCategory->name).' Listings and Prices' : 'All Listings and Prices')
@section('content') @section('content')
@php @include('listing::partials.index-content')
$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>
@endsection @endsection

View File

@ -42,32 +42,32 @@
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}"> <form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
@csrf @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' }}"> <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> </button>
</form> </form>
@if($listing->user && (int) $listing->user->id !== (int) auth()->id()) @if($listing->user && (int) $listing->user->id !== (int) auth()->id())
<form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}"> <form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}">
@csrf @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' }}"> <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> </button>
</form> </form>
@if($existingConversationId) @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"> <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> </a>
@else @else
<form method="POST" action="{{ route('conversations.start', $listing) }}"> <form method="POST" action="{{ route('conversations.start', $listing) }}">
@csrf @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"> <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> </button>
</form> </form>
@endif @endif
@endif @endif
@else @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"> <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> </a>
@endauth @endauth
</div> </div>
@ -92,7 +92,7 @@
@endif @endif
@if(($presentableCustomFields ?? []) !== []) @if(($presentableCustomFields ?? []) !== [])
<div class="mt-6 border-t pt-4"> <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"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
@foreach($presentableCustomFields as $field) @foreach($presentableCustomFields as $field)
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2"> <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') @extends('app::layouts.app')
@section('title', trim((string) ($selectedCategory?->name ?? '')) !== '' ? trim((string) $selectedCategory->name).' Listings and Prices' : 'All Listings and Prices')
@section('content') @section('content')
@php @include('listing::partials.index-content')
$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>
@endsection @endsection

View File

@ -40,6 +40,12 @@
$referenceCode = '#'.str_pad((string) $listing->getKey(), 8, '0', STR_PAD_LEFT); $referenceCode = '#'.str_pad((string) $listing->getKey(), 8, '0', STR_PAD_LEFT);
$canContactSeller = $listing->user && (! auth()->check() || (int) auth()->id() !== (int) $listing->user_id); $canContactSeller = $listing->user && (! auth()->check() || (int) auth()->id() !== (int) $listing->user_id);
$isOwnListing = 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; $primaryContactHref = null;
$primaryContactLabel = 'Call'; $primaryContactLabel = 'Call';
@ -51,13 +57,6 @@
$primaryContactLabel = 'Email'; $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'); $reportEmail = config('mail.from.address', 'support@example.com');
$reportUrl = 'mailto:'.$reportEmail.'?subject='.rawurlencode('Report listing '.$referenceCode); $reportUrl = 'mailto:'.$reportEmail.'?subject='.rawurlencode('Report listing '.$referenceCode);
$shareUrl = route('listings.show', $listing); $shareUrl = route('listings.show', $listing);
@ -358,21 +357,12 @@
<div class="lt-row-2"> <div class="lt-row-2">
@if(! $listing->user) @if(! $listing->user)
<button type="button" class="lt-btn" disabled>Unavailable</button> <button type="button" class="lt-btn" disabled>Unavailable</button>
@elseif($canContactSeller) @elseif($canStartConversation)
@if($existingConversationId) <button type="button" class="lt-btn" data-inline-chat-open>Message</button>
<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($isOwnListing) @elseif($isOwnListing)
<button type="button" class="lt-btn" disabled>Your listing</button> <button type="button" class="lt-btn" disabled>Your listing</button>
@else @else
<a href="{{ route('login') }}" class="lt-btn">Message</a> <a href="{{ $loginRedirectRoute }}" class="lt-btn">Message</a>
@endif @endif
@if($primaryContactHref) @if($primaryContactHref)
@ -382,49 +372,21 @@
@endif @endif
</div> </div>
@if(! $listing->user) @if($listing->user && ! $isOwnListing)
<button type="button" class="lt-btn lt-btn-main" disabled>Unavailable</button> @auth
@elseif($canContactSeller) <form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}" class="lt-action-form">
@if($existingConversationId)
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="lt-btn lt-btn-main">
Make offer
</a>
@else
<form method="POST" action="{{ route('conversations.start', $listing) }}" class="lt-action-form">
@csrf @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> </form>
@endif @else
<a href="{{ $loginRedirectRoute }}" class="lt-btn lt-btn-outline">Save seller</a>
@endauth
@elseif($isOwnListing) @elseif($isOwnListing)
<button type="button" class="lt-btn lt-btn-main" disabled>Manage listing</button> <button type="button" class="lt-btn lt-btn-outline" disabled>Your account</button>
@else
<a href="{{ route('login') }}" class="lt-btn lt-btn-main">Make offer</a>
@endif @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> </div>
</section> </section>
@ -441,21 +403,12 @@
<div class="lt-mobile-actions-row"> <div class="lt-mobile-actions-row">
@if(! $listing->user) @if(! $listing->user)
<button type="button" class="lt-btn" disabled>Unavailable</button> <button type="button" class="lt-btn" disabled>Unavailable</button>
@elseif($canContactSeller) @elseif($canStartConversation)
@if($existingConversationId) <button type="button" class="lt-btn" data-inline-chat-open>Message</button>
<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($isOwnListing) @elseif($isOwnListing)
<button type="button" class="lt-btn" disabled>Your listing</button> <button type="button" class="lt-btn" disabled>Your listing</button>
@else @else
<a href="{{ route('login') }}" class="lt-btn">Message</a> <a href="{{ $loginRedirectRoute }}" class="lt-btn">Message</a>
@endif @endif
@if($primaryContactHref) @if($primaryContactHref)
@ -465,27 +418,76 @@
@endif @endif
</div> </div>
@if(! $listing->user) @if($listing->user && ! $isOwnListing)
<button type="button" class="lt-btn lt-btn-main" disabled>Unavailable</button> @auth
@elseif($canContactSeller) <form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}" class="lt-action-form">
@if($existingConversationId)
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="lt-btn lt-btn-main">
Make offer
</a>
@else
<form method="POST" action="{{ route('conversations.start', $listing) }}" class="lt-action-form">
@csrf @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> </form>
@endif @else
<a href="{{ $loginRedirectRoute }}" class="lt-btn lt-btn-outline">Save seller</a>
@endauth
@elseif($isOwnListing) @elseif($isOwnListing)
<button type="button" class="lt-btn lt-btn-main" disabled>Manage listing</button> <button type="button" class="lt-btn lt-btn-outline" disabled>Your account</button>
@else
<a href="{{ route('login') }}" class="lt-btn lt-btn-main">Make offer</a>
@endif @endif
</div> </div>
</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()) @if(($relatedListings ?? collect())->isNotEmpty() || ($themePillCategories ?? collect())->isNotEmpty())
<section class="lt-related"> <section class="lt-related">
@if(($relatedListings ?? collect())->isNotEmpty()) @if(($relatedListings ?? collect())->isNotEmpty())
@ -677,6 +679,138 @@
button.addEventListener('click', () => activate(button.dataset.tab || 'details')); 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> </script>
@endsection @endsection

View File

@ -57,12 +57,12 @@ class LocationSeeder extends Seeder
if ($value === 'us_ca') { if ($value === 'us_ca') {
$countries['US'] = [ $countries['US'] = [
'code' => 'US', 'code' => 'US',
'name' => 'Amerika Birleşik Devletleri', 'name' => 'United States',
'phone_code' => $phoneCode, 'phone_code' => $phoneCode,
]; ];
$countries['CA'] = [ $countries['CA'] = [
'code' => 'CA', 'code' => 'CA',
'name' => 'Kanada', 'name' => 'Canada',
'phone_code' => $phoneCode, 'phone_code' => $phoneCode,
]; ];
@ -72,12 +72,12 @@ class LocationSeeder extends Seeder
if ($value === 'ru_kz') { if ($value === 'ru_kz') {
$countries['RU'] = [ $countries['RU'] = [
'code' => 'RU', 'code' => 'RU',
'name' => 'Rusya', 'name' => 'Russia',
'phone_code' => $phoneCode, 'phone_code' => $phoneCode,
]; ];
$countries['KZ'] = [ $countries['KZ'] = [
'code' => 'KZ', 'code' => 'KZ',
'name' => 'Kazakistan', 'name' => 'Kazakhstan',
'phone_code' => $phoneCode, 'phone_code' => $phoneCode,
]; ];
@ -85,12 +85,9 @@ class LocationSeeder extends Seeder
} }
$key = 'filament-country-code-field::countries.' . $value; $key = 'filament-country-code-field::countries.' . $value;
$labelTr = trim((string) trans($key, [], 'tr'));
$labelEn = trim((string) trans($key, [], 'en')); $labelEn = trim((string) trans($key, [], 'en'));
$name = $labelTr !== '' && $labelTr !== $key $name = $labelEn !== '' && $labelEn !== $key ? $labelEn : strtoupper($value);
? $labelTr
: ($labelEn !== '' && $labelEn !== $key ? $labelEn : strtoupper($value));
$iso2 = strtoupper(explode('_', $value)[0] ?? $value); $iso2 = strtoupper(explode('_', $value)[0] ?? $value);
@ -122,67 +119,67 @@ class LocationSeeder extends Seeder
{ {
return [ return [
'Adana', 'Adana',
'Adıyaman', 'Adiyaman',
'Afyonkarahisar', 'Afyonkarahisar',
'Ağrı', 'Agri',
'Aksaray', 'Aksaray',
'Amasya', 'Amasya',
'Ankara', 'Ankara',
'Antalya', 'Antalya',
'Ardahan', 'Ardahan',
'Artvin', 'Artvin',
'Aydın', 'Aydin',
'Balıkesir', 'Balikesir',
'Bartın', 'Bartin',
'Batman', 'Batman',
'Bayburt', 'Bayburt',
'Bilecik', 'Bilecik',
'Bingöl', 'Bingol',
'Bitlis', 'Bitlis',
'Bolu', 'Bolu',
'Burdur', 'Burdur',
'Bursa', 'Bursa',
'Çanakkale', 'Canakkale',
'Çankırı', 'Cankiri',
'Çorum', 'Corum',
'Denizli', 'Denizli',
'Diyarbakır', 'Diyarbakir',
'Düzce', 'Duzce',
'Edirne', 'Edirne',
'Elazığ', 'Elazig',
'Erzincan', 'Erzincan',
'Erzurum', 'Erzurum',
'Eskişehir', 'Eskisehir',
'Gaziantep', 'Gaziantep',
'Giresun', 'Giresun',
'Gümüşhane', 'Gumushane',
'Hakkari', 'Hakkari',
'Hatay', 'Hatay',
'Iğdır', 'Igdir',
'Isparta', 'Isparta',
'İstanbul', 'Istanbul',
'İzmir', 'Izmir',
'Kahramanmaraş', 'Kahramanmaras',
'Karabük', 'Karabuk',
'Karaman', 'Karaman',
'Kars', 'Kars',
'Kastamonu', 'Kastamonu',
'Kayseri', 'Kayseri',
'Kilis', 'Kilis',
'Kırıkkale', 'Kirikkale',
'Kırklareli', 'Kirklareli',
'Kıehir', 'Kirsehir',
'Kocaeli', 'Kocaeli',
'Konya', 'Konya',
'Kütahya', 'Kutahya',
'Malatya', 'Malatya',
'Manisa', 'Manisa',
'Mardin', 'Mardin',
'Mersin', 'Mersin',
'Muğla', 'Mugla',
'Muş', 'Mus',
'Nevşehir', 'Nevsehir',
'Niğde', 'Nigde',
'Ordu', 'Ordu',
'Osmaniye', 'Osmaniye',
'Rize', 'Rize',
@ -191,13 +188,13 @@ class LocationSeeder extends Seeder
'Siirt', 'Siirt',
'Sinop', 'Sinop',
'Sivas', 'Sivas',
'Şanlıurfa', 'Sanliurfa',
'Şırnak', 'Sirnak',
'Tekirdağ', 'Tekirdag',
'Tokat', 'Tokat',
'Trabzon', 'Trabzon',
'Tunceli', 'Tunceli',
'Uşak', 'Usak',
'Van', 'Van',
'Yalova', 'Yalova',
'Yozgat', 'Yozgat',

View File

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

View File

@ -207,7 +207,7 @@ class PanelController extends Controller
$this->guardListingOwner($request, $listing); $this->guardListingOwner($request, $listing);
$listing->delete(); $listing->delete();
return back()->with('success', 'İlan kaldırıldı.'); return back()->with('success', 'Listing removed.');
} }
public function markListingAsSold(Request $request, Listing $listing): RedirectResponse public function markListingAsSold(Request $request, Listing $listing): RedirectResponse
@ -217,7 +217,7 @@ class PanelController extends Controller
'status' => 'sold', 'status' => 'sold',
])->save(); ])->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 public function republishListing(Request $request, Listing $listing): RedirectResponse
@ -228,7 +228,7 @@ class PanelController extends Controller
'expires_at' => now()->addDays(30), 'expires_at' => now()->addDays(30),
])->save(); ])->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 private function guardListingOwner(Request $request, Listing $listing): void

View File

@ -514,13 +514,10 @@ class PanelQuickListingForm extends Component
$fieldRules[] = $isRequired ? 'required' : 'nullable'; $fieldRules[] = $isRequired ? 'required' : 'nullable';
} }
$fieldRules[] = match ($type) { $fieldRules = [
ListingCustomField::TYPE_TEXT => 'string|max:255', ...$fieldRules,
ListingCustomField::TYPE_TEXTAREA => 'string|max:2000', ...$this->customFieldTypeRules($type),
ListingCustomField::TYPE_NUMBER => 'numeric', ];
ListingCustomField::TYPE_DATE => 'date',
default => 'sometimes',
};
if ($type === ListingCustomField::TYPE_SELECT) { if ($type === ListingCustomField::TYPE_SELECT) {
$options = collect($field['options'] ?? [])->map(fn ($option): string => (string) $option)->all(); $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 private function createListing(): Listing
{ {
$user = auth()->user(); $user = auth()->user();

View File

@ -34,7 +34,7 @@ class AppServiceProvider extends ServiceProvider
$availableLocales = config('app.available_locales', ['en']); $availableLocales = config('app.available_locales', ['en']);
$localeLabels = [ $localeLabels = [
'en' => 'English', 'en' => 'English',
'tr' => 'Türkçe', 'tr' => 'Turkish',
]; ];
LanguageSwitch::configureUsing(function (LanguageSwitch $switch) use ($availableLocales, $localeLabels): void { LanguageSwitch::configureUsing(function (LanguageSwitch $switch) use ($availableLocales, $localeLabels): void {

View File

@ -1314,6 +1314,194 @@ summary::-webkit-details-marker {
font-weight: 600; 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 { .lt-mobile-actions {
position: fixed; position: fixed;
right: 0; right: 0;
@ -1860,6 +2048,22 @@ summary::-webkit-details-marker {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px; 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) { @media (max-width: 480px) {

View File

@ -1,7 +1 @@
import './bootstrap'; 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"> <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"> <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> <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"> <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"> <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> </a>
@auth @auth
<form method="POST" action="{{ route('logout') }}"> <form method="POST" action="{{ route('logout') }}">
@csrf @csrf
<button type="submit" class="px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700"> <button type="submit" class="px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700">
Çıkış Yap Log out
</button> </button>
</form> </form>
@else @else
<a href="{{ route('login') }}" class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700"> <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> </a>
@endauth @endauth
</div> </div>

View File

@ -138,7 +138,7 @@
type="button" type="button"
data-home-slide-prev 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" 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> <span aria-hidden="true"></span>
</button> </button>
@ -158,7 +158,7 @@
type="button" type="button"
data-home-slide-next 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" 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> <span aria-hidden="true"></span>
</button> </button>
@ -216,9 +216,9 @@
<section> <section>
<div class="flex items-center justify-between mb-3"> <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"> <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> </a>
</div> </div>
<div class="relative"> <div class="relative">
@ -226,7 +226,7 @@
type="button" type="button"
data-trend-prev 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" 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"> <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"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 18-6-6 6-6"/>
@ -296,7 +296,7 @@
type="button" type="button"
data-trend-next 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" 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"> <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"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 18 6-6-6-6"/>
@ -307,7 +307,7 @@
<section> <section>
<div class="flex items-center justify-between mb-4"> <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"> <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>
<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> </a>
<div class="absolute top-3 left-3 flex items-center gap-2"> <div class="absolute top-3 left-3 flex items-center gap-2">
@if($listing->is_featured) @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 @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>
<div class="absolute top-3 right-3"> <div class="absolute top-3 right-3">
@auth @auth
@ -357,17 +357,17 @@
<p class="text-3xl font-extrabold tracking-tight text-slate-900">{{ $priceLabel }}</p> <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> <h3 class="text-xl font-semibold text-slate-800 mt-1 truncate">{{ $listing->title }}</h3>
</div> </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>
<div class="mt-5 flex items-center justify-between text-sm text-slate-500"> <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> <span>{{ $listing->created_at->diffForHumans() }}</span>
</div> </div>
</div> </div>
</article> </article>
@empty @empty
<div class="col-span-2 border border-dashed border-slate-300 bg-white rounded-2xl py-20 text-center text-slate-500"> <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> </div>
@endforelse @endforelse
</div> </div>
@ -377,15 +377,15 @@
<div class="grid md:grid-cols-[1fr,auto] gap-6 items-center"> <div class="grid md:grid-cols-[1fr,auto] gap-6 items-center">
<div> <div>
<h2 class="text-3xl md:text-4xl font-extrabold">{{ __('messages.sell_something') }}</h2> <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> </div>
@auth @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"> <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> </a>
@else @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"> <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> </a>
@endauth @endauth
</div> </div>

View File

@ -40,15 +40,15 @@
$availableLocales = config('app.available_locales', ['en']); $availableLocales = config('app.available_locales', ['en']);
$localeLabels = [ $localeLabels = [
'en' => 'English', 'en' => 'English',
'tr' => 'Türkçe', 'tr' => 'Turkish',
'ar' => 'العربية', 'ar' => 'Arabic',
'zh' => '中文', 'zh' => 'Chinese',
'es' => 'Español', 'es' => 'Español',
'fr' => 'Français', 'fr' => 'French',
'de' => 'Deutsch', 'de' => 'German',
'pt' => 'Português', 'pt' => 'Portuguese',
'ru' => 'Русский', 'ru' => 'Russian',
'ja' => '日本語', 'ja' => 'Japanese',
]; ];
$headerCategories = collect($headerNavCategories ?? [])->values(); $headerCategories = collect($headerNavCategories ?? [])->values();
$menuBrowseLinks = collect([ $menuBrowseLinks = collect([

View File

@ -1,96 +1,76 @@
@extends('app::layouts.app') @extends('app::layouts.app')
@section('title', 'İlanlarım') @section('title', 'My Listings')
@section('content') @section('content')
@php @php
$statusTabs = [ $statusTabs = [
[ [
'key' => 'all', 'key' => 'all',
'label' => 'Tüm İlanlar', 'label' => 'All Listings',
'count' => (int) ($counts['all'] ?? 0), 'count' => (int) ($counts['all'] ?? 0),
'description' => 'Hesabındaki tüm ilanlar', 'description' => 'All listings in your account',
], ],
[ [
'key' => 'sold', 'key' => 'sold',
'label' => 'Satıldı', 'label' => 'Sold',
'count' => (int) ($counts['sold'] ?? 0), 'count' => (int) ($counts['sold'] ?? 0),
'description' => 'Kapanan satışlar', 'description' => 'Closed sales',
], ],
[ [
'key' => 'expired', 'key' => 'expired',
'label' => 'Süresi Doldu', 'label' => 'Expired',
'count' => (int) ($counts['expired'] ?? 0), 'count' => (int) ($counts['expired'] ?? 0),
'description' => 'Yeniden yayın bekleyenler', 'description' => 'Waiting to be republished',
], ],
]; ];
$overviewCards = [ $overviewCards = [
[ [
'label' => 'Toplam İlan', 'label' => 'Total Listings',
'value' => (int) ($counts['all'] ?? 0), '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), '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), '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), 'value' => (int) ($counts['expired'] ?? 0),
'hint' => 'Yeniden yayın bekleyen ilanlar', 'hint' => 'Listings waiting to be republished',
], ],
]; ];
$hasFilters = $search !== '' || $status !== 'all'; $hasFilters = $search !== '' || $status !== 'all';
$pendingCount = (int) ($counts['pending'] ?? 0);
@endphp @endphp
<div class="listings-dashboard-page mx-auto max-w-[1320px] px-4 py-6 md:py-8"> <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)]"> <div class="grid gap-6 xl:grid-cols-[300px,minmax(0,1fr)]">
<aside class="listings-dashboard-sidebar space-y-6"> <aside class="listings-dashboard-sidebar space-y-6">
@include('panel.partials.sidebar', ['activeMenu' => 'listings']) @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> </aside>
<section class="space-y-6"> <section class="space-y-6">
<div class="listings-dashboard-hero"> <div class="listings-dashboard-hero">
<div class="min-w-0"> <div class="min-w-0">
<p class="account-section-kicker">Panel</p> <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"> <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> </p>
</div> </div>
<div class="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center"> <div class="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center">
@if ($hasFilters) @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 @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>
</div> </div>
@ -107,10 +87,10 @@
<div class="listings-dashboard-filter-shell"> <div class="listings-dashboard-filter-shell">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between"> <div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div> <div>
<p class="account-section-kicker">Filtrele</p> <p class="account-section-kicker">Filter</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">Arama ve durum</h2> <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"> <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> </p>
</div> </div>
@ -122,11 +102,11 @@
type="text" type="text"
name="search" name="search"
value="{{ $search }}" value="{{ $search }}"
placeholder="İlan başlığına göre ara" placeholder="Search by listing title"
class="listings-dashboard-search-input" class="listings-dashboard-search-input"
> >
<input type="hidden" name="status" value="{{ $status }}"> <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> </form>
</div> </div>
@ -157,7 +137,7 @@
$viewCount = (int) ($listing->view_count ?? 0); $viewCount = (int) ($listing->view_count ?? 0);
$publishedAt = $listing->panelPublishedAt(); $publishedAt = $listing->panelPublishedAt();
$publishedLabel = $publishedAt?->format('d.m.Y') ?? '-'; $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); $videoCount = (int) ($listing->videos_count ?? 0);
$readyVideoCount = (int) ($listing->ready_videos_count ?? 0); $readyVideoCount = (int) ($listing->ready_videos_count ?? 0);
$pendingVideoCount = (int) ($listing->pending_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"> <img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="h-full w-full object-cover">
@else @else
<div class="listings-dashboard-placeholder"> <div class="listings-dashboard-placeholder">
<span>Görsel Yok</span> <span>No image</span>
</div> </div>
@endif @endif
</a> </a>
@ -198,21 +178,21 @@
<div class="grid gap-3 md:grid-cols-3"> <div class="grid gap-3 md:grid-cols-3">
<div class="listings-dashboard-info-card"> <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> <strong>{{ $publishedLabel }}</strong>
<span>İlk görünür olduğu kayıt tarihi</span> <span>First visible date</span>
</div> </div>
<div class="listings-dashboard-info-card"> <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> <strong>{{ $expiresLabel }}</strong>
<span>{{ $listing->panelExpirySummary() }}</span> <span>{{ $listing->panelExpirySummary() }}</span>
</div> </div>
<div class="listings-dashboard-info-card"> <div class="listings-dashboard-info-card">
<span class="listings-dashboard-info-label">Etkileşim</span> <span class="listings-dashboard-info-label">Engagement</span>
<strong>{{ number_format($viewCount) }} görüntülenme</strong> <strong>{{ number_format($viewCount) }} views</strong>
<span>{{ number_format($favoriteCount) }} favori</span> <span>{{ number_format($favoriteCount) }} saved</span>
</div> </div>
</div> </div>
</div> </div>
@ -226,18 +206,18 @@
@if ($listing->statusValue() === 'expired') @if ($listing->statusValue() === 'expired')
<div class="listings-dashboard-alert is-danger"> <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> </div>
@elseif ($listing->statusValue() === 'pending') @elseif ($listing->statusValue() === 'pending')
<div class="listings-dashboard-alert is-warning"> <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> </div>
@endif @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-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"> <div class="flex flex-wrap gap-3">
<a href="{{ route('listings.show', $listing) }}" class="account-secondary-button">İlanı Gör</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">Düzenle</a> <a href="{{ route('panel.listings.edit', $listing) }}" class="account-primary-button">Edit</a>
</div> </div>
<div class="flex flex-wrap gap-3"> <div class="flex flex-wrap gap-3">
@ -245,14 +225,14 @@
<form method="POST" action="{{ route('panel.listings.republish', $listing) }}"> <form method="POST" action="{{ route('panel.listings.republish', $listing) }}">
@csrf @csrf
<button type="submit" class="listings-dashboard-text-button"> <button type="submit" class="listings-dashboard-text-button">
Yeniden Yayınla Republish
</button> </button>
</form> </form>
@elseif ($listing->statusValue() !== 'sold') @elseif ($listing->statusValue() !== 'sold')
<form method="POST" action="{{ route('panel.listings.mark-sold', $listing) }}"> <form method="POST" action="{{ route('panel.listings.mark-sold', $listing) }}">
@csrf @csrf
<button type="submit" class="listings-dashboard-text-button"> <button type="submit" class="listings-dashboard-text-button">
Satıldı İşaretle Mark as Sold
</button> </button>
</form> </form>
@endif @endif
@ -260,7 +240,7 @@
<form method="POST" action="{{ route('panel.listings.destroy', $listing) }}"> <form method="POST" action="{{ route('panel.listings.destroy', $listing) }}">
@csrf @csrf
<button type="submit" class="listings-dashboard-text-button is-danger"> <button type="submit" class="listings-dashboard-text-button is-danger">
İlanı Kaldır Remove Listing
</button> </button>
</form> </form>
</div> </div>
@ -274,17 +254,17 @@
</article> </article>
@empty @empty
<div class="listings-dashboard-empty"> <div class="listings-dashboard-empty">
<p class="account-section-kicker">Boş durum</p> <p class="account-section-kicker">Empty State</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">Bu filtreye uygun ilan bulunamadı</h2> <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"> <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> </p>
<div class="mt-6 flex flex-col gap-3 sm:flex-row"> <div class="mt-6 flex flex-col gap-3 sm:flex-row">
@if ($hasFilters) @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 @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>
</div> </div>
@endforelse @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 @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"> <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"> <nav class="space-y-1.5">
@foreach ($primaryItems as $item) @foreach ($primaryItems as $item)
<a <a

View File

@ -8,12 +8,12 @@
@include('panel.partials.sidebar', ['activeMenu' => 'videos']) @include('panel.partials.sidebar', ['activeMenu' => 'videos'])
<section class="space-y-4"> <section class="space-y-4">
<div class="panel-surface p-6"> @include('panel.partials.page-header', [
<div class="flex flex-col gap-1 mb-5"> 'title' => 'Videos',
<h1 class="text-2xl font-semibold text-slate-900">Videos</h1> 'description' => 'Upload listing videos and manage processing from one frontend workspace.',
<p class="text-sm text-slate-500">Upload listing videos and manage processing from the frontend panel.</p> ])
</div>
<div class="panel-surface p-6">
@if (session('success')) @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"> <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') }} {{ session('success') }}

View File

@ -14,13 +14,13 @@ Route::get('/dashboard', fn () => auth()->check()
Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () { Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
Route::get('/', [PanelController::class, 'index'])->name('index'); 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('/create-listing', [PanelController::class, 'create'])->name('listings.create');
Route::get('/ilanlarim/{listing}/duzenle', [PanelController::class, 'editListing'])->name('listings.edit'); Route::get('/my-listings/{listing}/edit', [PanelController::class, 'editListing'])->name('listings.edit');
Route::put('/ilanlarim/{listing}', [PanelController::class, 'updateListing'])->name('listings.update'); Route::put('/my-listings/{listing}', [PanelController::class, 'updateListing'])->name('listings.update');
Route::post('/ilanlarim/{listing}/kaldir', [PanelController::class, 'destroyListing'])->name('listings.destroy'); Route::post('/my-listings/{listing}/remove', [PanelController::class, 'destroyListing'])->name('listings.destroy');
Route::post('/ilanlarim/{listing}/satildi', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold'); Route::post('/my-listings/{listing}/mark-sold', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold');
Route::post('/ilanlarim/{listing}/yeniden-yayinla', [PanelController::class, 'republishListing'])->name('listings.republish'); Route::post('/my-listings/{listing}/republish', [PanelController::class, 'republishListing'])->name('listings.republish');
Route::get('/videos', [PanelController::class, 'videos'])->name('videos.index'); Route::get('/videos', [PanelController::class, 'videos'])->name('videos.index');
Route::post('/videos', [PanelController::class, 'storeVideo'])->name('videos.store'); Route::post('/videos', [PanelController::class, 'storeVideo'])->name('videos.store');
Route::get('/videos/{video}/edit', [PanelController::class, 'editVideo'])->name('videos.edit'); Route::get('/videos/{video}/edit', [PanelController::class, 'editVideo'])->name('videos.edit');