mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
Document listing and category fixes
This commit is contained in:
parent
747e7410f4
commit
afe9cf4080
@ -3,6 +3,9 @@ APP_ENV=local
|
|||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost:8000
|
APP_URL=http://localhost:8000
|
||||||
|
OC_THEME=otoplus
|
||||||
|
OC_THEME_LISTING=otoplus
|
||||||
|
OC_THEME_CATEGORY=otoplus
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=en
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
|
|||||||
@ -1,21 +1,34 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Modules\Category\Http\Controllers;
|
namespace Modules\Category\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
|
use Modules\Theme\Support\ThemeManager;
|
||||||
|
|
||||||
class CategoryController extends Controller
|
class CategoryController extends Controller
|
||||||
{
|
{
|
||||||
|
public function __construct(private ThemeManager $themes)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$categories = Category::whereNull('parent_id')->with('children')->where('is_active', true)->get();
|
$categories = Category::rootTreeWithActiveChildren();
|
||||||
return view('category::index', compact('categories'));
|
|
||||||
|
return view($this->themes->view('category', 'index'), compact('categories'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(Category $category)
|
public function show(Category $category)
|
||||||
{
|
{
|
||||||
$listings = $category->listings()->where('status', 'active')->paginate(12);
|
$category->loadMissing([
|
||||||
return view('category::show', compact('category', 'listings'));
|
'children' => fn ($query) => $query->active()->ordered(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$listings = $category->activeListings()
|
||||||
|
->with('category:id,name')
|
||||||
|
->latest('id')
|
||||||
|
->paginate(12);
|
||||||
|
|
||||||
|
return view($this->themes->view('category', 'show'), compact('category', 'listings'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Modules\Category\Models;
|
namespace Modules\Category\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Spatie\Activitylog\LogOptions;
|
use Spatie\Activitylog\LogOptions;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
|
||||||
@ -37,8 +39,65 @@ class Category extends Model
|
|||||||
return $this->hasMany(\Modules\Listing\Models\Listing::class);
|
return $this->hasMany(\Modules\Listing\Models\Listing::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function scopeActive(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->where('is_active', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeOrdered(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->orderBy('sort_order')->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function filterOptions(): Collection
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->ordered()
|
||||||
|
->get(['id', 'name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function themePills(int $limit = 8): Collection
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->ordered()
|
||||||
|
->limit($limit)
|
||||||
|
->get(['id', 'name', 'slug']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function rootTreeWithActiveChildren(): Collection
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->whereNull('parent_id')
|
||||||
|
->with([
|
||||||
|
'children' => fn (Builder $query) => $query->active()->ordered(),
|
||||||
|
])
|
||||||
|
->ordered()
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function breadcrumbTrail(): Collection
|
||||||
|
{
|
||||||
|
$trail = collect();
|
||||||
|
$current = $this;
|
||||||
|
|
||||||
|
while ($current) {
|
||||||
|
$trail->prepend($current);
|
||||||
|
$current = $current->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $trail;
|
||||||
|
}
|
||||||
|
|
||||||
public function listingCustomFields(): HasMany
|
public function listingCustomFields(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(\Modules\Listing\Models\ListingCustomField::class);
|
return $this->hasMany(\Modules\Listing\Models\ListingCustomField::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function activeListings(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(\Modules\Listing\Models\Listing::class)->where('status', 'active');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
Modules/Category/resources/views/themes/README.md
Normal file
10
Modules/Category/resources/views/themes/README.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Category Theme Contract
|
||||||
|
|
||||||
|
Active category template is resolved from `config('theme.modules.category')`.
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
- `themes/{theme}/index.blade.php`
|
||||||
|
- `themes/{theme}/show.blade.php`
|
||||||
|
|
||||||
|
Then set `OC_THEME_CATEGORY={theme}`.
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
@section('content')
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-6">{{ __('messages.categories') }}</h1>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
@foreach($categories as $category)
|
||||||
|
<a href="{{ route('categories.show', $category) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
|
||||||
|
<div class="text-4xl mb-3">{{ $category->icon ?? '📦' }}</div>
|
||||||
|
<h3 class="font-semibold text-gray-900">{{ $category->name }}</h3>
|
||||||
|
<p class="text-gray-500 text-sm mt-1">{{ $category->children->count() }} subcategories</p>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
@section('content')
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-3xl font-bold">{{ $category->icon ?? '' }} {{ $category->name }}</h1>
|
||||||
|
@if($category->description)<p class="text-gray-600 mt-2">{{ $category->description }}</p>@endif
|
||||||
|
</div>
|
||||||
|
@if($category->children->count())
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
@foreach($category->children as $child)
|
||||||
|
<a href="{{ route('categories.show', $child) }}" class="bg-blue-50 rounded-lg p-4 text-center hover:bg-blue-100 transition">
|
||||||
|
<h3 class="font-medium text-blue-800">{{ $child->name }}</h3>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<h2 class="text-xl font-bold mb-4">Listings in {{ $category->name }}</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
@forelse($listings as $listing)
|
||||||
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="font-semibold">{{ $listing->title }}</h3>
|
||||||
|
<p class="text-green-600 font-bold">{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}</p>
|
||||||
|
<a href="{{ route('listings.show', $listing) }}" class="mt-2 block text-blue-600 hover:underline">View →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="text-gray-500 col-span-3">No listings in this category yet.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">{{ $listings->links() }}</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
@section('content')
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-6">{{ __('messages.categories') }}</h1>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
|
@foreach($categories as $category)
|
||||||
|
<a href="{{ route('categories.show', $category) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
|
||||||
|
<div class="text-4xl mb-3">{{ $category->icon ?? '📦' }}</div>
|
||||||
|
<h3 class="font-semibold text-gray-900">{{ $category->name }}</h3>
|
||||||
|
<p class="text-gray-500 text-sm mt-1">{{ $category->children->count() }} subcategories</p>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
@section('content')
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-3xl font-bold">{{ $category->icon ?? '' }} {{ $category->name }}</h1>
|
||||||
|
@if($category->description)<p class="text-gray-600 mt-2">{{ $category->description }}</p>@endif
|
||||||
|
</div>
|
||||||
|
@if($category->children->count())
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
@foreach($category->children as $child)
|
||||||
|
<a href="{{ route('categories.show', $child) }}" class="bg-blue-50 rounded-lg p-4 text-center hover:bg-blue-100 transition">
|
||||||
|
<h3 class="font-medium text-blue-800">{{ $child->name }}</h3>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<h2 class="text-xl font-bold mb-4">Listings in {{ $category->name }}</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
@forelse($listings as $listing)
|
||||||
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="font-semibold">{{ $listing->title }}</h3>
|
||||||
|
<p class="text-green-600 font-bold">{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}</p>
|
||||||
|
<a href="{{ route('listings.show', $listing) }}" class="mt-2 block text-blue-600 hover:underline">View →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<p class="text-gray-500 col-span-3">No listings in this category yet.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">{{ $listings->links() }}</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@ -22,18 +22,10 @@ return new class extends Migration
|
|||||||
$table->unsignedInteger('sort_order')->default(0);
|
$table->unsignedInteger('sort_order')->default(0);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
Schema::table('listings', function (Blueprint $table): void {
|
|
||||||
$table->json('custom_fields')->nullable()->after('images');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::table('listings', function (Blueprint $table): void {
|
|
||||||
$table->dropColumn('custom_fields');
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::dropIfExists('listing_custom_fields');
|
Schema::dropIfExists('listing_custom_fields');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('listings', function (Blueprint $table): void {
|
|
||||||
if (! Schema::hasColumn('listings', 'latitude')) {
|
|
||||||
$table->decimal('latitude', 10, 7)->nullable()->after('country');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! Schema::hasColumn('listings', 'longitude')) {
|
|
||||||
$table->decimal('longitude', 10, 7)->nullable()->after('latitude');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('listings', function (Blueprint $table): void {
|
|
||||||
if (Schema::hasColumn('listings', 'longitude')) {
|
|
||||||
$table->dropColumn('longitude');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Schema::hasColumn('listings', 'latitude')) {
|
|
||||||
$table->dropColumn('latitude');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('listings', function (Blueprint $table): void {
|
|
||||||
if (! Schema::hasColumn('listings', 'view_count')) {
|
|
||||||
$table->unsignedInteger('view_count')->default(0)->after('is_featured');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('listings', function (Blueprint $table): void {
|
|
||||||
if (Schema::hasColumn('listings', 'view_count')) {
|
|
||||||
$table->dropColumn('view_count');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -4,39 +4,106 @@ namespace Modules\Listing\Http\Controllers;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Conversation;
|
use App\Models\Conversation;
|
||||||
use App\Models\FavoriteSearch;
|
use App\Models\FavoriteSearch;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Modules\Location\Models\City;
|
||||||
|
use Modules\Location\Models\Country;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
||||||
|
use Modules\Theme\Support\ThemeManager;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class ListingController extends Controller
|
class ListingController extends Controller
|
||||||
{
|
{
|
||||||
|
public function __construct(private ThemeManager $themes)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$search = trim((string) request('search', ''));
|
$search = trim((string) request('search', ''));
|
||||||
|
|
||||||
$categoryId = request()->integer('category');
|
$categoryId = request()->integer('category');
|
||||||
$categoryId = $categoryId > 0 ? $categoryId : null;
|
$categoryId = $categoryId > 0 ? $categoryId : null;
|
||||||
|
|
||||||
$listings = Listing::query()
|
$countryId = request()->integer('country');
|
||||||
->publicFeed()
|
$countryId = $countryId > 0 ? $countryId : null;
|
||||||
|
|
||||||
|
$cityId = request()->integer('city');
|
||||||
|
$cityId = $cityId > 0 ? $cityId : null;
|
||||||
|
|
||||||
|
$minPriceInput = trim((string) request('min_price', ''));
|
||||||
|
$maxPriceInput = trim((string) request('max_price', ''));
|
||||||
|
$minPrice = is_numeric($minPriceInput) ? max((float) $minPriceInput, 0) : null;
|
||||||
|
$maxPrice = is_numeric($maxPriceInput) ? max((float) $maxPriceInput, 0) : null;
|
||||||
|
|
||||||
|
$dateFilter = (string) request('date_filter', 'all');
|
||||||
|
$allowedDateFilters = ['all', 'today', 'week', 'month'];
|
||||||
|
if (! in_array($dateFilter, $allowedDateFilters, true)) {
|
||||||
|
$dateFilter = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
$sort = (string) request('sort', 'smart');
|
||||||
|
$allowedSorts = ['smart', 'newest', 'oldest', 'price_asc', 'price_desc'];
|
||||||
|
if (! in_array($sort, $allowedSorts, true)) {
|
||||||
|
$sort = 'smart';
|
||||||
|
}
|
||||||
|
|
||||||
|
$countries = collect();
|
||||||
|
$cities = collect();
|
||||||
|
$selectedCountryName = null;
|
||||||
|
$selectedCityName = null;
|
||||||
|
|
||||||
|
$this->resolveLocationFilters(
|
||||||
|
$countryId,
|
||||||
|
$cityId,
|
||||||
|
$countries,
|
||||||
|
$cities,
|
||||||
|
$selectedCountryName,
|
||||||
|
$selectedCityName
|
||||||
|
);
|
||||||
|
|
||||||
|
$listingsQuery = Listing::query()
|
||||||
|
->where('status', 'active')
|
||||||
->with('category:id,name')
|
->with('category:id,name')
|
||||||
->when($search !== '', function ($query) use ($search): void {
|
->searchTerm($search)
|
||||||
$query->where(function ($searchQuery) use ($search): void {
|
->forCategory($categoryId)
|
||||||
$searchQuery
|
->when($selectedCountryName, fn ($query) => $query->where('country', $selectedCountryName))
|
||||||
->where('title', 'like', "%{$search}%")
|
->when($selectedCityName, fn ($query) => $query->where('city', $selectedCityName))
|
||||||
->orWhere('description', 'like', "%{$search}%")
|
->when(! is_null($minPrice), fn ($query) => $query->whereNotNull('price')->where('price', '>=', $minPrice))
|
||||||
->orWhere('city', 'like', "%{$search}%")
|
->when(! is_null($maxPrice), fn ($query) => $query->whereNotNull('price')->where('price', '<=', $maxPrice));
|
||||||
->orWhere('country', 'like', "%{$search}%");
|
|
||||||
});
|
$this->applyDateFilter($listingsQuery, $dateFilter);
|
||||||
})
|
$this->applySorting($listingsQuery, $sort);
|
||||||
->when($categoryId, fn ($query) => $query->where('category_id', $categoryId))
|
|
||||||
->paginate(12)
|
$listings = $listingsQuery
|
||||||
|
->paginate(16)
|
||||||
->withQueryString();
|
->withQueryString();
|
||||||
|
|
||||||
$categories = Category::query()
|
$categories = Category::query()
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
|
->whereNull('parent_id')
|
||||||
|
->withCount([
|
||||||
|
'listings as active_listings_count' => fn ($query) => $query->where('status', 'active'),
|
||||||
|
])
|
||||||
|
->with([
|
||||||
|
'children' => fn ($query) => $query
|
||||||
|
->where('is_active', true)
|
||||||
|
->withCount([
|
||||||
|
'listings as active_listings_count' => fn ($childQuery) => $childQuery->where('status', 'active'),
|
||||||
|
])
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('name'),
|
||||||
|
])
|
||||||
|
->orderBy('sort_order')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'name']);
|
->get(['id', 'name', 'parent_id']);
|
||||||
|
|
||||||
|
$selectedCategory = $categoryId
|
||||||
|
? Category::query()->whereKey($categoryId)->first(['id', 'name'])
|
||||||
|
: null;
|
||||||
|
|
||||||
$favoriteListingIds = [];
|
$favoriteListingIds = [];
|
||||||
$isCurrentSearchSaved = false;
|
$isCurrentSearchSaved = false;
|
||||||
@ -70,10 +137,19 @@ class ListingController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('listing::index', compact(
|
return view($this->themes->view('listing', 'index'), compact(
|
||||||
'listings',
|
'listings',
|
||||||
'search',
|
'search',
|
||||||
'categoryId',
|
'categoryId',
|
||||||
|
'countryId',
|
||||||
|
'cityId',
|
||||||
|
'minPriceInput',
|
||||||
|
'maxPriceInput',
|
||||||
|
'dateFilter',
|
||||||
|
'sort',
|
||||||
|
'countries',
|
||||||
|
'cities',
|
||||||
|
'selectedCategory',
|
||||||
'categories',
|
'categories',
|
||||||
'favoriteListingIds',
|
'favoriteListingIds',
|
||||||
'isCurrentSearchSaved',
|
'isCurrentSearchSaved',
|
||||||
@ -91,11 +167,22 @@ class ListingController extends Controller
|
|||||||
$listing->refresh();
|
$listing->refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
$listing->loadMissing('user:id,name,email');
|
$listing->loadMissing([
|
||||||
|
'user:id,name,email',
|
||||||
|
'category:id,name,parent_id,slug',
|
||||||
|
'category.parent:id,name,parent_id,slug',
|
||||||
|
'category.parent.parent:id,name,parent_id,slug',
|
||||||
|
]);
|
||||||
$presentableCustomFields = ListingCustomFieldSchemaBuilder::presentableValues(
|
$presentableCustomFields = ListingCustomFieldSchemaBuilder::presentableValues(
|
||||||
$listing->category_id ? (int) $listing->category_id : null,
|
$listing->category_id ? (int) $listing->category_id : null,
|
||||||
$listing->custom_fields ?? [],
|
$listing->custom_fields ?? [],
|
||||||
);
|
);
|
||||||
|
$gallery = $listing->themeGallery();
|
||||||
|
$relatedListings = $listing->relatedSuggestions(12);
|
||||||
|
$themePillCategories = Category::themePills(10);
|
||||||
|
$breadcrumbCategories = $listing->category
|
||||||
|
? $listing->category->breadcrumbTrail()
|
||||||
|
: collect();
|
||||||
|
|
||||||
$isListingFavorited = false;
|
$isListingFavorited = false;
|
||||||
$isSellerFavorited = false;
|
$isSellerFavorited = false;
|
||||||
@ -117,19 +204,23 @@ class ListingController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($listing->user_id && (int) $listing->user_id !== $userId) {
|
if ($listing->user_id && (int) $listing->user_id !== $userId) {
|
||||||
$existingConversationId = Conversation::query()
|
$existingConversationId = Conversation::buyerListingConversationId(
|
||||||
->where('listing_id', $listing->getKey())
|
(int) $listing->getKey(),
|
||||||
->where('buyer_id', $userId)
|
$userId,
|
||||||
->value('id');
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('listing::show', compact(
|
return view($this->themes->view('listing', 'show'), compact(
|
||||||
'listing',
|
'listing',
|
||||||
'isListingFavorited',
|
'isListingFavorited',
|
||||||
'isSellerFavorited',
|
'isSellerFavorited',
|
||||||
'presentableCustomFields',
|
'presentableCustomFields',
|
||||||
'existingConversationId',
|
'existingConversationId',
|
||||||
|
'gallery',
|
||||||
|
'relatedListings',
|
||||||
|
'themePillCategories',
|
||||||
|
'breadcrumbCategories',
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,4 +243,101 @@ class ListingController extends Controller
|
|||||||
->route('panel.listings.create')
|
->route('panel.listings.create')
|
||||||
->with('success', 'İlan oluşturma ekranına yönlendirildin.');
|
->with('success', 'İlan oluşturma ekranına yönlendirildin.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveLocationFilters(
|
||||||
|
?int &$countryId,
|
||||||
|
?int &$cityId,
|
||||||
|
Collection &$countries,
|
||||||
|
Collection &$cities,
|
||||||
|
?string &$selectedCountryName,
|
||||||
|
?string &$selectedCityName
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
if (! Schema::hasTable('countries') || ! Schema::hasTable('cities')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$countries = Country::query()
|
||||||
|
->where('is_active', true)
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name']);
|
||||||
|
|
||||||
|
$selectedCountry = $countryId
|
||||||
|
? $countries->firstWhere('id', $countryId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (! $selectedCountry && $countryId) {
|
||||||
|
$selectedCountry = Country::query()->whereKey($countryId)->first(['id', 'name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$selectedCity = null;
|
||||||
|
if ($cityId) {
|
||||||
|
$selectedCity = City::query()->whereKey($cityId)->first(['id', 'name', 'country_id']);
|
||||||
|
if (! $selectedCity) {
|
||||||
|
$cityId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($selectedCity && ! $selectedCountry) {
|
||||||
|
$countryId = (int) $selectedCity->country_id;
|
||||||
|
$selectedCountry = Country::query()->whereKey($countryId)->first(['id', 'name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($selectedCountry) {
|
||||||
|
$selectedCountryName = (string) $selectedCountry->name;
|
||||||
|
$cities = City::query()
|
||||||
|
->where('country_id', $selectedCountry->id)
|
||||||
|
->where('is_active', true)
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'country_id']);
|
||||||
|
|
||||||
|
if ($cities->isEmpty()) {
|
||||||
|
$cities = City::query()
|
||||||
|
->where('country_id', $selectedCountry->id)
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'country_id']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$countryId = null;
|
||||||
|
$cities = collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($selectedCity) {
|
||||||
|
if ($selectedCountry && (int) $selectedCity->country_id !== (int) $selectedCountry->id) {
|
||||||
|
$selectedCity = null;
|
||||||
|
$cityId = null;
|
||||||
|
} else {
|
||||||
|
$selectedCityName = (string) $selectedCity->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
$countryId = null;
|
||||||
|
$cityId = null;
|
||||||
|
$selectedCountryName = null;
|
||||||
|
$selectedCityName = null;
|
||||||
|
$countries = collect();
|
||||||
|
$cities = collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyDateFilter($query, string $dateFilter): void
|
||||||
|
{
|
||||||
|
match ($dateFilter) {
|
||||||
|
'today' => $query->where('created_at', '>=', Carbon::now()->startOfDay()),
|
||||||
|
'week' => $query->where('created_at', '>=', Carbon::now()->subDays(7)),
|
||||||
|
'month' => $query->where('created_at', '>=', Carbon::now()->subDays(30)),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applySorting($query, string $sort): void
|
||||||
|
{
|
||||||
|
match ($sort) {
|
||||||
|
'newest' => $query->reorder()->orderByDesc('created_at'),
|
||||||
|
'oldest' => $query->reorder()->orderBy('created_at'),
|
||||||
|
'price_asc' => $query->reorder()->orderByRaw('price is null')->orderBy('price'),
|
||||||
|
'price_desc' => $query->reorder()->orderByRaw('price is null')->orderByDesc('price'),
|
||||||
|
default => $query->reorder()->orderByDesc('is_featured')->orderByDesc('created_at'),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
namespace Modules\Listing\Models;
|
namespace Modules\Listing\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Modules\Listing\States\ListingStatus;
|
use Modules\Listing\States\ListingStatus;
|
||||||
use Modules\Listing\Support\ListingPanelHelper;
|
use Modules\Listing\Support\ListingPanelHelper;
|
||||||
@ -76,6 +77,76 @@ class Listing extends Model implements HasMedia
|
|||||||
->orderByDesc('created_at');
|
->orderByDesc('created_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function scopeSearchTerm(Builder $query, string $search): Builder
|
||||||
|
{
|
||||||
|
$search = trim($search);
|
||||||
|
|
||||||
|
if ($search === '') {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->where(function (Builder $searchQuery) use ($search): void {
|
||||||
|
$searchQuery
|
||||||
|
->where('title', 'like', "%{$search}%")
|
||||||
|
->orWhere('description', 'like', "%{$search}%")
|
||||||
|
->orWhere('city', 'like', "%{$search}%")
|
||||||
|
->orWhere('country', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeForCategory(Builder $query, ?int $categoryId): Builder
|
||||||
|
{
|
||||||
|
if (! $categoryId) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->where('category_id', $categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function themeGallery(): array
|
||||||
|
{
|
||||||
|
$mediaUrls = $this->getMedia('listing-images')
|
||||||
|
->map(fn ($media): string => $media->getUrl())
|
||||||
|
->filter(fn (string $url): bool => $url !== '')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($mediaUrls !== []) {
|
||||||
|
return $mediaUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($this->images ?? [])
|
||||||
|
->filter(fn ($value): bool => is_string($value) && trim($value) !== '')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relatedSuggestions(int $limit = 8): Collection
|
||||||
|
{
|
||||||
|
$baseQuery = static::query()
|
||||||
|
->publicFeed()
|
||||||
|
->with('category:id,name')
|
||||||
|
->whereKeyNot($this->getKey());
|
||||||
|
|
||||||
|
$primary = (clone $baseQuery)
|
||||||
|
->forCategory($this->category_id ? (int) $this->category_id : null)
|
||||||
|
->limit($limit)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($primary->count() >= $limit) {
|
||||||
|
return $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
$missing = $limit - $primary->count();
|
||||||
|
$excludeIds = $primary->pluck('id')->push($this->getKey())->all();
|
||||||
|
$fallback = (clone $baseQuery)
|
||||||
|
->whereNotIn('id', $excludeIds)
|
||||||
|
->limit($missing)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return $primary->concat($fallback)->values();
|
||||||
|
}
|
||||||
|
|
||||||
public static function createFromFrontend(array $data, null | int | string $userId): self
|
public static function createFromFrontend(array $data, null | int | string $userId): self
|
||||||
{
|
{
|
||||||
$baseSlug = Str::slug((string) ($data['title'] ?? 'listing'));
|
$baseSlug = Str::slug((string) ($data['title'] ?? 'listing'));
|
||||||
|
|||||||
@ -7,7 +7,7 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('listings', function (Blueprint $table) {
|
Schema::create('listings', function (Blueprint $table): void {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('title');
|
$table->string('title');
|
||||||
$table->string('slug')->unique();
|
$table->string('slug')->unique();
|
||||||
@ -18,12 +18,16 @@ return new class extends Migration
|
|||||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||||
$table->string('status')->default('active');
|
$table->string('status')->default('active');
|
||||||
$table->json('images')->nullable();
|
$table->json('images')->nullable();
|
||||||
|
$table->json('custom_fields')->nullable();
|
||||||
$table->string('contact_phone')->nullable();
|
$table->string('contact_phone')->nullable();
|
||||||
$table->string('contact_email')->nullable();
|
$table->string('contact_email')->nullable();
|
||||||
$table->boolean('is_featured')->default(false);
|
$table->boolean('is_featured')->default(false);
|
||||||
|
$table->unsignedInteger('view_count')->default(0);
|
||||||
$table->timestamp('expires_at')->nullable();
|
$table->timestamp('expires_at')->nullable();
|
||||||
$table->string('city')->nullable();
|
$table->string('city')->nullable();
|
||||||
$table->string('country')->nullable();
|
$table->string('country')->nullable();
|
||||||
|
$table->decimal('latitude', 10, 7)->nullable();
|
||||||
|
$table->decimal('longitude', 10, 7)->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,117 +1,478 @@
|
|||||||
@extends('app::layouts.app')
|
@extends('app::layouts.app')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
@php
|
||||||
<div class="flex flex-col lg:flex-row lg:items-center gap-4 mb-6">
|
$totalListings = (int) $listings->total();
|
||||||
<h1 class="text-3xl font-bold text-slate-900 mr-auto">{{ __('messages.listings') }}</h1>
|
$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
|
||||||
|
|
||||||
<form method="GET" action="{{ route('listings.index') }}" class="flex flex-wrap items-center gap-2">
|
<style>
|
||||||
@if($search !== '')
|
.listing-filter-card {
|
||||||
<input type="hidden" name="search" value="{{ $search }}">
|
border: 1px solid #d7dbe7;
|
||||||
@endif
|
border-radius: 14px;
|
||||||
<select name="category" class="h-10 min-w-44 border border-slate-300 rounded-lg px-3 text-sm text-slate-700">
|
background: #ffffff;
|
||||||
<option value="">Kategori</option>
|
}
|
||||||
@foreach($categories as $category)
|
|
||||||
<option value="{{ $category->id }}" @selected((int) $categoryId === (int) $category->id)>{{ $category->name }}</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="h-10 px-4 bg-slate-700 text-white text-sm font-semibold rounded-lg hover:bg-slate-800 transition">Filtrele</button>
|
|
||||||
@if($categoryId)
|
|
||||||
<a href="{{ route('listings.index', array_filter(['search' => $search])) }}" class="h-10 px-4 inline-flex items-center border border-slate-300 text-sm font-semibold rounded-lg hover:bg-slate-50 transition">
|
|
||||||
Sıfırla
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@auth
|
.listing-card {
|
||||||
@php
|
border: 1px solid #d7dbe7;
|
||||||
$canSaveSearch = $search !== '' || !is_null($categoryId);
|
border-radius: 12px;
|
||||||
@endphp
|
overflow: hidden;
|
||||||
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4 mb-6 flex flex-col sm:flex-row sm:items-center gap-3">
|
background: #ffffff;
|
||||||
<div class="mr-auto text-sm text-slate-600">
|
transition: box-shadow .2s ease, transform .2s ease;
|
||||||
Bu aramayı favorilere ekleyerek daha sonra hızlıca açabilirsin.
|
}
|
||||||
</div>
|
|
||||||
<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-lg text-sm font-semibold {{ $isCurrentSearchSaved ? 'bg-emerald-100 text-emerald-700 cursor-default' : ($canSaveSearch ? 'bg-rose-500 text-white hover:bg-rose-600 transition' : 'bg-slate-200 text-slate-400 cursor-not-allowed') }}" @disabled($isCurrentSearchSaved || ! $canSaveSearch)>
|
|
||||||
{{ $isCurrentSearchSaved ? 'Arama Favorilerde' : ($canSaveSearch ? 'Aramayı Favorilere Ekle' : 'Filtre seç') }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<a href="{{ route('favorites.index', ['tab' => 'searches']) }}" class="h-10 px-4 inline-flex items-center border border-slate-300 text-sm font-semibold rounded-lg hover:bg-white transition">
|
|
||||||
Favori Aramalar
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
@endauth
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
.listing-card:hover {
|
||||||
@forelse($listings as $listing)
|
box-shadow: 0 16px 32px rgba(22, 29, 57, 0.11);
|
||||||
@php
|
transform: translateY(-2px);
|
||||||
$listingImage = $listing->getFirstMediaUrl('listing-images');
|
}
|
||||||
$isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
|
|
||||||
$conversationId = $conversationListingMap[$listing->id] ?? null;
|
|
||||||
@endphp
|
|
||||||
<div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition border border-slate-200">
|
|
||||||
<div class="bg-gray-200 h-48 flex items-center justify-center relative">
|
|
||||||
@if($listingImage)
|
|
||||||
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover">
|
|
||||||
@else
|
|
||||||
<svg class="w-16 h-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="absolute top-3 right-3">
|
.listing-title {
|
||||||
@auth
|
display: -webkit-box;
|
||||||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
-webkit-box-orient: vertical;
|
||||||
@csrf
|
-webkit-line-clamp: 2;
|
||||||
<button type="submit" class="w-9 h-9 rounded-full grid place-items-center transition {{ $isFavorited ? 'bg-rose-500 text-white' : 'bg-white/95 text-slate-500 hover:text-rose-500' }}" aria-label="Favoriye ekle">
|
overflow: hidden;
|
||||||
♥
|
}
|
||||||
</button>
|
</style>
|
||||||
</form>
|
|
||||||
@else
|
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
|
||||||
<a href="{{ route('login') }}" class="w-9 h-9 rounded-full bg-white/95 text-slate-500 hover:text-rose-500 grid place-items-center transition" aria-label="Giriş yap">
|
<h1 class="text-[30px] leading-tight font-extrabold 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-[26px] font-extrabold 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($totalListings, 0, ',', '.') }}</span>
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
|
||||||
</div>
|
@foreach($categories as $category)
|
||||||
</div>
|
@php
|
||||||
<div class="p-4">
|
$childCount = (int) $category->children->sum('active_listings_count');
|
||||||
@if($listing->is_featured)
|
$categoryCount = (int) $category->active_listings_count + $childCount;
|
||||||
<span class="bg-yellow-100 text-yellow-800 text-xs font-medium px-2 py-1 rounded">Featured</span>
|
$isSelectedParent = (int) $categoryId === (int) $category->id;
|
||||||
@endif
|
$categoryUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||||
<h3 class="font-semibold text-gray-900 mt-2 truncate">{{ $listing->title }}</h3>
|
'category' => $category->id,
|
||||||
<p class="text-green-600 font-bold text-lg mt-1">
|
]), $normalizeQuery));
|
||||||
@if($listing->price) {{ number_format($listing->price, 0) }} {{ $listing->currency }} @else Free @endif
|
@endphp
|
||||||
</p>
|
<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' }}">
|
||||||
<p class="text-xs text-slate-500 mt-1 truncate">{{ $listing->category?->name ?: 'Kategori yok' }}</p>
|
<span>{{ $category->name }}</span>
|
||||||
<p class="text-gray-500 text-sm mt-1">{{ $listing->city }}, {{ $listing->country }}</p>
|
<span>{{ number_format($categoryCount, 0, ',', '.') }}</span>
|
||||||
<div class="mt-3 grid grid-cols-1 gap-2">
|
</a>
|
||||||
<a href="{{ route('listings.show', $listing) }}" class="block text-center bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition">View</a>
|
|
||||||
@auth
|
@foreach($category->children as $childCategory)
|
||||||
@if($listing->user_id && (int) $listing->user_id !== (int) auth()->id())
|
@php
|
||||||
@if($conversationId)
|
$isSelectedChild = (int) $categoryId === (int) $childCategory->id;
|
||||||
<a href="{{ route('panel.inbox.index', ['conversation' => $conversationId]) }}" class="block text-center border border-rose-300 text-rose-600 py-2 rounded hover:bg-rose-50 transition text-sm font-semibold">
|
$childUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||||
Sohbete Git
|
'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_listings_count, 0, ',', '.') }}</span>
|
||||||
</a>
|
</a>
|
||||||
@else
|
@endforeach
|
||||||
<form method="POST" action="{{ route('conversations.start', $listing) }}">
|
@endforeach
|
||||||
@csrf
|
</div>
|
||||||
<button type="submit" class="w-full border border-rose-300 text-rose-600 py-2 rounded hover:bg-rose-50 transition text-sm font-semibold">
|
</section>
|
||||||
Mesaj Gönder
|
|
||||||
</button>
|
<form method="GET" action="{{ route('listings.index') }}" class="listing-filter-card p-4 space-y-5">
|
||||||
</form>
|
@if($search !== '')
|
||||||
@endif
|
<input type="hidden" name="search" value="{{ $search }}">
|
||||||
@endif
|
@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($totalListings, 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
|
@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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
@empty
|
@if($listings->isEmpty())
|
||||||
<div class="md:col-span-2 lg:col-span-3 xl:col-span-4 border border-dashed border-slate-300 rounded-xl py-14 text-center text-slate-500">
|
<div class="listing-filter-card py-14 text-center text-slate-500">
|
||||||
Bu filtreye uygun ilan bulunamadı.
|
Bu filtreye uygun ilan bulunamadı.
|
||||||
</div>
|
</div>
|
||||||
@endforelse
|
@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-[29px] leading-none font-extrabold 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>
|
||||||
<div class="mt-8">{{ $listings->links() }}</div>
|
|
||||||
</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
|
||||||
|
|||||||
20
Modules/Listing/resources/views/themes/README.md
Normal file
20
Modules/Listing/resources/views/themes/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Listing Theme Contract
|
||||||
|
|
||||||
|
Active template is resolved from `config('theme.modules.listing')`.
|
||||||
|
|
||||||
|
Directory structure:
|
||||||
|
|
||||||
|
- `themes/{theme}/index.blade.php`
|
||||||
|
- `themes/{theme}/show.blade.php`
|
||||||
|
|
||||||
|
Fallback order:
|
||||||
|
|
||||||
|
1. `listing::themes.{active}.{view}`
|
||||||
|
2. `listing::themes.default.{view}`
|
||||||
|
3. `listing::{view}`
|
||||||
|
|
||||||
|
To add a new theme:
|
||||||
|
|
||||||
|
1. Create `Modules/Listing/resources/views/themes/{your-theme}/index.blade.php`.
|
||||||
|
2. Create `Modules/Listing/resources/views/themes/{your-theme}/show.blade.php`.
|
||||||
|
3. Set `OC_THEME_LISTING={your-theme}` in `.env`.
|
||||||
478
Modules/Listing/resources/views/themes/default/index.blade.php
Normal file
478
Modules/Listing/resources/views/themes/default/index.blade.php
Normal file
@ -0,0 +1,478 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
@section('content')
|
||||||
|
@php
|
||||||
|
$totalListings = (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
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.listing-filter-card {
|
||||||
|
border: 1px solid #d7dbe7;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-card {
|
||||||
|
border: 1px solid #d7dbe7;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
transition: box-shadow .2s ease, transform .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-card:hover {
|
||||||
|
box-shadow: 0 16px 32px rgba(22, 29, 57, 0.11);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-title {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
|
||||||
|
<h1 class="text-[30px] leading-tight font-extrabold 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-[26px] font-extrabold 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($totalListings, 0, ',', '.') }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
@foreach($categories as $category)
|
||||||
|
@php
|
||||||
|
$childCount = (int) $category->children->sum('active_listings_count');
|
||||||
|
$categoryCount = (int) $category->active_listings_count + $childCount;
|
||||||
|
$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_listings_count, 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($totalListings, 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-[29px] leading-none font-extrabold 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
|
||||||
115
Modules/Listing/resources/views/themes/default/show.blade.php
Normal file
115
Modules/Listing/resources/views/themes/default/show.blade.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
@section('content')
|
||||||
|
@php
|
||||||
|
$title = trim((string) ($listing->title ?? ''));
|
||||||
|
$displayTitle = ($title !== '' && preg_match('/[\pL\pN]/u', $title)) ? $title : 'Untitled listing';
|
||||||
|
|
||||||
|
$city = trim((string) ($listing->city ?? ''));
|
||||||
|
$country = trim((string) ($listing->country ?? ''));
|
||||||
|
$location = implode(', ', array_filter([$city, $country], fn ($value) => $value !== ''));
|
||||||
|
|
||||||
|
$description = trim((string) ($listing->description ?? ''));
|
||||||
|
$displayDescription = ($description !== '' && preg_match('/[\pL\pN]/u', $description))
|
||||||
|
? $description
|
||||||
|
: 'No description provided.';
|
||||||
|
|
||||||
|
$hasPrice = !is_null($listing->price);
|
||||||
|
$priceValue = $hasPrice ? (float) $listing->price : null;
|
||||||
|
@endphp
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="max-w-4xl mx-auto">
|
||||||
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
|
<div class="bg-gray-200 h-96 flex items-center justify-center">
|
||||||
|
<svg class="w-24 h-24 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900">{{ $displayTitle }}</h1>
|
||||||
|
<span class="text-3xl font-bold text-green-600">
|
||||||
|
@if($hasPrice)
|
||||||
|
@if($priceValue > 0)
|
||||||
|
{{ number_format($priceValue, 0) }} {{ $listing->currency ?? 'USD' }}
|
||||||
|
@else
|
||||||
|
Free
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
Price on request
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||||
|
@auth
|
||||||
|
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold transition {{ $isListingFavorited ? 'bg-rose-100 text-rose-700' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||||
|
{{ $isListingFavorited ? '♥ Favorilerde' : '♡ Favoriye Ekle' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@if($listing->user && (int) $listing->user->id !== (int) auth()->id())
|
||||||
|
<form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold transition {{ $isSellerFavorited ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||||
|
{{ $isSellerFavorited ? 'Satıcı Favorilerde' : 'Satıcıyı Takip Et' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@if($existingConversationId)
|
||||||
|
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-rose-100 text-rose-700 hover:bg-rose-200 transition">
|
||||||
|
Sohbete Git
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<form method="POST" action="{{ route('conversations.start', $listing) }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-rose-500 text-white hover:bg-rose-600 transition">
|
||||||
|
Satıcıya Mesaj Gönder
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-slate-100 text-slate-700 hover:bg-slate-200 transition">
|
||||||
|
Giriş yap ve favorile
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 mt-2">{{ $location !== '' ? $location : 'Location not specified' }}</p>
|
||||||
|
<p class="text-gray-500 text-sm">Posted {{ $listing->created_at?->diffForHumans() ?? 'recently' }}</p>
|
||||||
|
<div class="mt-4 border-t pt-4">
|
||||||
|
<h2 class="font-semibold text-lg mb-2">Description</h2>
|
||||||
|
<p class="text-gray-700">{{ $displayDescription }}</p>
|
||||||
|
</div>
|
||||||
|
@if(($presentableCustomFields ?? []) !== [])
|
||||||
|
<div class="mt-6 border-t pt-4">
|
||||||
|
<h2 class="font-semibold text-lg mb-3">İlan Özellikleri</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
@foreach($presentableCustomFields as $field)
|
||||||
|
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
|
||||||
|
<p class="text-xs uppercase tracking-wide text-slate-500">{{ $field['label'] }}</p>
|
||||||
|
<p class="text-sm font-medium text-slate-800 mt-1">{{ $field['value'] }}</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="mt-6 bg-gray-50 rounded-lg p-4">
|
||||||
|
<h2 class="font-semibold text-lg mb-3">Contact Seller</h2>
|
||||||
|
@if($listing->user)
|
||||||
|
<p class="text-gray-700"><span class="font-medium">Name:</span> {{ $listing->user->name }}</p>
|
||||||
|
@endif
|
||||||
|
@if($listing->contact_phone)
|
||||||
|
<p class="text-gray-700"><span class="font-medium">Phone:</span> {{ $listing->contact_phone }}</p>
|
||||||
|
@endif
|
||||||
|
@if($listing->contact_email)
|
||||||
|
<p class="text-gray-700"><span class="font-medium">Email:</span> {{ $listing->contact_email }}</p>
|
||||||
|
@endif
|
||||||
|
@if(!$listing->contact_phone && !$listing->contact_email)
|
||||||
|
<p class="text-gray-700">No contact details provided.</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="mt-6">
|
||||||
|
<a href="{{ route('listings.index') }}" class="text-blue-600 hover:underline">← Back to listings</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
478
Modules/Listing/resources/views/themes/otoplus/index.blade.php
Normal file
478
Modules/Listing/resources/views/themes/otoplus/index.blade.php
Normal file
@ -0,0 +1,478 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
@section('content')
|
||||||
|
@php
|
||||||
|
$totalListings = (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
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.listing-filter-card {
|
||||||
|
border: 1px solid #d7dbe7;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-card {
|
||||||
|
border: 1px solid #d7dbe7;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
transition: box-shadow .2s ease, transform .2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-card:hover {
|
||||||
|
box-shadow: 0 16px 32px rgba(22, 29, 57, 0.11);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.listing-title {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
|
||||||
|
<h1 class="text-[30px] leading-tight font-extrabold 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-[26px] font-extrabold 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($totalListings, 0, ',', '.') }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
@foreach($categories as $category)
|
||||||
|
@php
|
||||||
|
$childCount = (int) $category->children->sum('active_listings_count');
|
||||||
|
$categoryCount = (int) $category->active_listings_count + $childCount;
|
||||||
|
$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_listings_count, 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($totalListings, 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-[29px] leading-none font-extrabold 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
|
||||||
416
Modules/Listing/resources/views/themes/otoplus/show.blade.php
Normal file
416
Modules/Listing/resources/views/themes/otoplus/show.blade.php
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
@php
|
||||||
|
$title = trim((string) ($listing->title ?? ''));
|
||||||
|
$displayTitle = $title !== '' ? $title : 'İlan başlığı yok';
|
||||||
|
|
||||||
|
$priceLabel = 'Fiyat sorunuz';
|
||||||
|
if (! is_null($listing->price)) {
|
||||||
|
$priceValue = (float) $listing->price;
|
||||||
|
$priceLabel = $priceValue > 0
|
||||||
|
? number_format($priceValue, 0, ',', '.').' '.($listing->currency ?: 'TL')
|
||||||
|
: 'Ücretsiz';
|
||||||
|
}
|
||||||
|
|
||||||
|
$locationLabel = collect([$listing->city, $listing->country])
|
||||||
|
->filter(fn ($value) => is_string($value) && trim($value) !== '')
|
||||||
|
->implode(', ');
|
||||||
|
|
||||||
|
$publishedAt = $listing->created_at?->format('d M Y');
|
||||||
|
$galleryImages = collect($gallery ?? [])->values()->all();
|
||||||
|
$initialGalleryImage = $galleryImages[0] ?? null;
|
||||||
|
|
||||||
|
$sellerName = trim((string) ($listing->user?->name ?? 'Satıcı'));
|
||||||
|
$sellerInitial = strtoupper(substr($sellerName, 0, 1));
|
||||||
|
$sellerMemberText = $listing->user?->created_at
|
||||||
|
? $listing->user->created_at->format('M Y').' tarihinden beri üye'
|
||||||
|
: 'Yeni üye';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lt-wrap { max-width: 1320px; margin: 0 auto; padding: 24px 16px 46px; }
|
||||||
|
.lt-breadcrumb { display: flex; flex-wrap: wrap; gap: 8px; font-size: 13px; color: #6b7280; margin-bottom: 14px; }
|
||||||
|
.lt-breadcrumb a { color: #4b5563; text-decoration: none; }
|
||||||
|
.lt-breadcrumb span:last-child { color: #111827; font-weight: 700; }
|
||||||
|
.lt-grid { display: grid; grid-template-columns: minmax(0, 1fr) 352px; gap: 18px; align-items: start; }
|
||||||
|
.lt-card { border: 1px solid #d8dce4; border-radius: 14px; background: #f7f7f8; box-shadow: 0 1px 2px rgba(16, 24, 40, .05); }
|
||||||
|
|
||||||
|
.lt-gallery-main { position: relative; border-radius: 10px; background: #1f2937; overflow: hidden; min-height: 440px; }
|
||||||
|
.lt-gallery-main img { width: 100%; height: 100%; object-fit: cover; min-height: 440px; }
|
||||||
|
.lt-gallery-main-empty { min-height: 440px; display: grid; place-items: center; color: #cbd5e1; font-size: 14px; }
|
||||||
|
.lt-gallery-nav { position: absolute; top: 50%; transform: translateY(-50%); width: 44px; height: 44px; border: 0; border-radius: 999px; background: rgba(255,255,255,.92); color: #111827; display: grid; place-items: center; cursor: pointer; }
|
||||||
|
.lt-gallery-nav[data-gallery-prev] { left: 14px; }
|
||||||
|
.lt-gallery-nav[data-gallery-next] { right: 14px; }
|
||||||
|
.lt-gallery-top { position: absolute; top: 12px; left: 12px; right: 12px; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.lt-badge { border-radius: 999px; background: #ffd814; color: #111827; font-size: 12px; font-weight: 700; padding: 6px 10px; }
|
||||||
|
.lt-icon-btn { width: 38px; height: 38px; border: 0; border-radius: 999px; background: rgba(17, 24, 39, .86); color: #fff; display: inline-flex; align-items: center; justify-content: center; }
|
||||||
|
|
||||||
|
.lt-thumbs { display: flex; gap: 10px; overflow-x: auto; padding: 12px 0 2px; }
|
||||||
|
.lt-thumb { width: 86px; min-width: 86px; height: 64px; border: 2px solid transparent; border-radius: 10px; overflow: hidden; background: #d1d5db; cursor: pointer; }
|
||||||
|
.lt-thumb.is-active { border-color: #ff3a59; }
|
||||||
|
.lt-thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
|
||||||
|
.lt-media-card { padding: 14px; }
|
||||||
|
.lt-detail-card { margin-top: 14px; padding: 18px 20px; }
|
||||||
|
.lt-price-row { display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between; align-items: flex-start; }
|
||||||
|
.lt-price { font-size: 46px; line-height: 1; font-weight: 900; color: #0f172a; }
|
||||||
|
.lt-title { margin-top: 8px; font-size: 21px; font-weight: 700; color: #111827; }
|
||||||
|
.lt-meta { text-align: right; color: #4b5563; font-size: 14px; }
|
||||||
|
.lt-meta strong { color: #111827; font-weight: 700; }
|
||||||
|
|
||||||
|
.lt-credit { margin-top: 14px; border: 1px solid #e3e7ee; border-radius: 12px; padding: 14px; background: #fafafb; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
|
.lt-credit h4 { margin: 0; font-size: 20px; color: #0f172a; }
|
||||||
|
.lt-credit p { margin: 3px 0 0; font-size: 14px; color: #4b5563; }
|
||||||
|
.lt-tag { border-radius: 999px; background: #2f80ed; color: #fff; font-size: 13px; font-weight: 700; padding: 7px 12px; }
|
||||||
|
|
||||||
|
.lt-section-title { margin: 18px 0 10px; font-size: 30px; font-weight: 900; color: #111827; }
|
||||||
|
.lt-features { border-top: 1px solid #e2e8f0; margin-top: 12px; }
|
||||||
|
.lt-feature-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; border-top: 1px solid #e7ebf2; padding: 12px 0; }
|
||||||
|
.lt-feature-row:first-child { border-top: 0; }
|
||||||
|
.lt-f-item { display: flex; justify-content: space-between; gap: 8px; color: #334155; font-size: 15px; }
|
||||||
|
.lt-f-item strong { color: #111827; font-weight: 800; text-align: right; }
|
||||||
|
|
||||||
|
.lt-side-card { position: sticky; top: 96px; padding: 16px; }
|
||||||
|
.lt-seller-head { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.lt-avatar { width: 44px; height: 44px; border-radius: 999px; background: #f3f4f6; color: #111827; display: grid; place-items: center; font-weight: 800; }
|
||||||
|
.lt-seller-name { margin: 0; font-size: 31px; font-weight: 800; color: #111827; line-height: 1.1; }
|
||||||
|
.lt-seller-meta { margin-top: 2px; font-size: 13px; color: #6b7280; }
|
||||||
|
|
||||||
|
.lt-actions { margin-top: 14px; display: grid; gap: 10px; }
|
||||||
|
.lt-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||||
|
.lt-btn { height: 46px; border-radius: 999px; border: 1px solid #f3ced6; background: #f8e6ea; color: #e11d48; font-size: 20px; font-weight: 800; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; }
|
||||||
|
.lt-btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||||
|
.lt-btn-main { border: 0; background: #ff3a59; color: #fff; width: 100%; }
|
||||||
|
.lt-btn-soft { border-color: #efdde1; background: #f5eaed; }
|
||||||
|
.lt-btn-outline { border-color: #d4d8e0; background: #fff; color: #334155; }
|
||||||
|
|
||||||
|
.lt-report { margin-top: 16px; height: 54px; border: 1px solid #e3e7ee; border-radius: 999px; background: #f7f7f8; color: #e11d48; font-size: 16px; font-weight: 700; display: grid; place-items: center; text-decoration: none; }
|
||||||
|
.lt-policy { margin-top: 16px; text-align: center; color: #6b7280; font-size: 13px; }
|
||||||
|
|
||||||
|
.lt-related { margin-top: 26px; }
|
||||||
|
.lt-related-head { display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.lt-related-title { font-size: 30px; font-weight: 900; margin: 0; }
|
||||||
|
.lt-scroll-wrap { position: relative; margin-top: 14px; }
|
||||||
|
.lt-scroll-track { display: flex; gap: 12px; overflow-x: auto; scroll-behavior: smooth; padding: 2px 2px 8px; }
|
||||||
|
.lt-rel-card { min-width: 232px; width: 232px; border: 1px solid #d8dce4; border-radius: 10px; background: #f7f7f8; overflow: hidden; text-decoration: none; color: inherit; }
|
||||||
|
.lt-rel-photo { height: 168px; background: #d1d5db; }
|
||||||
|
.lt-rel-photo img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.lt-rel-body { padding: 10px; }
|
||||||
|
.lt-rel-price { font-size: 32px; font-weight: 900; color: #111827; line-height: 1.1; }
|
||||||
|
.lt-rel-title { margin-top: 4px; font-size: 20px; font-weight: 700; color: #111827; line-height: 1.3; min-height: 52px; }
|
||||||
|
.lt-rel-city { margin-top: 6px; font-size: 13px; color: #6b7280; }
|
||||||
|
|
||||||
|
.lt-scroll-btn { position: absolute; top: 42%; transform: translateY(-50%); width: 44px; height: 44px; border: 0; border-radius: 999px; background: rgba(255,255,255,.92); box-shadow: 0 1px 4px rgba(15,23,42,.18); display: grid; place-items: center; cursor: pointer; }
|
||||||
|
.lt-scroll-btn.prev { left: -16px; }
|
||||||
|
.lt-scroll-btn.next { right: -16px; }
|
||||||
|
|
||||||
|
.lt-pill-wrap { margin-top: 20px; }
|
||||||
|
.lt-pill-title { margin: 0 0 10px; font-size: 30px; font-weight: 900; }
|
||||||
|
.lt-pills { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||||
|
.lt-pill { border: 1px solid #d4d8e0; background: #f4f5f7; border-radius: 999px; padding: 8px 14px; color: #374151; text-decoration: none; font-size: 14px; font-weight: 600; }
|
||||||
|
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
.lt-grid { grid-template-columns: 1fr; }
|
||||||
|
.lt-side-card { position: static; }
|
||||||
|
.lt-scroll-btn { display: none; }
|
||||||
|
.lt-price { font-size: 39px; }
|
||||||
|
.lt-seller-name, .lt-section-title, .lt-related-title, .lt-pill-title { font-size: 24px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.lt-wrap { padding: 16px 10px 30px; }
|
||||||
|
.lt-detail-card, .lt-media-card, .lt-side-card { padding: 12px; }
|
||||||
|
.lt-gallery-main, .lt-gallery-main img, .lt-gallery-main-empty { min-height: 260px; }
|
||||||
|
.lt-feature-row { grid-template-columns: 1fr; gap: 10px; }
|
||||||
|
.lt-price-row { flex-direction: column; }
|
||||||
|
.lt-meta { text-align: left; }
|
||||||
|
.lt-rel-card { min-width: 196px; width: 196px; }
|
||||||
|
.lt-rel-photo { height: 140px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="lt-wrap">
|
||||||
|
<nav class="lt-breadcrumb" aria-label="breadcrumb">
|
||||||
|
<a href="{{ route('home') }}">Anasayfa</a>
|
||||||
|
@foreach(($breadcrumbCategories ?? collect()) as $crumb)
|
||||||
|
<span>›</span>
|
||||||
|
<a href="{{ route('categories.show', $crumb) }}">{{ $crumb->name }}</a>
|
||||||
|
@endforeach
|
||||||
|
<span>›</span>
|
||||||
|
<span>{{ $displayTitle }}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="lt-grid">
|
||||||
|
<div>
|
||||||
|
<section class="lt-card lt-media-card" data-gallery>
|
||||||
|
<div class="lt-gallery-main">
|
||||||
|
<div class="lt-gallery-top">
|
||||||
|
<span class="lt-badge">Öne Çıkan</span>
|
||||||
|
@auth
|
||||||
|
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="lt-icon-btn" aria-label="Favoriye ekle">
|
||||||
|
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 21l-1.4-1.3C5.4 15 2 12 2 8.4 2 5.5 4.3 3.2 7.2 3.2c1.7 0 3.3.8 4.4 2.1 1.1-1.3 2.8-2.1 4.4-2.1C18.9 3.2 21.2 5.5 21.2 8.4c0 3.6-3.4 6.6-8.6 11.3L12 21z"/></svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($initialGalleryImage)
|
||||||
|
<img src="{{ $initialGalleryImage }}" alt="{{ $displayTitle }}" data-gallery-main>
|
||||||
|
@else
|
||||||
|
<div class="lt-gallery-main-empty" data-gallery-main-empty>Görsel bulunamadı</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(count($galleryImages) > 1)
|
||||||
|
<button type="button" class="lt-gallery-nav" data-gallery-prev aria-label="Önceki">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m15 18-6-6 6-6"/></svg>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="lt-gallery-nav" data-gallery-next aria-label="Sonraki">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></svg>
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($galleryImages !== [])
|
||||||
|
<div class="lt-thumbs" data-gallery-thumbs>
|
||||||
|
@foreach($galleryImages as $index => $image)
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="lt-thumb {{ $index === 0 ? 'is-active' : '' }}"
|
||||||
|
data-gallery-thumb
|
||||||
|
data-gallery-index="{{ $index }}"
|
||||||
|
data-gallery-src="{{ $image }}"
|
||||||
|
>
|
||||||
|
<img src="{{ $image }}" alt="{{ $displayTitle }} {{ $index + 1 }}">
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="lt-card lt-detail-card">
|
||||||
|
<div class="lt-price-row">
|
||||||
|
<div>
|
||||||
|
<div class="lt-price">{{ $priceLabel }}</div>
|
||||||
|
<div class="lt-title">{{ $displayTitle }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="lt-meta">
|
||||||
|
<div><strong>{{ $locationLabel !== '' ? $locationLabel : 'Konum belirtilmedi' }}</strong></div>
|
||||||
|
<div>{{ $publishedAt ?? '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lt-credit">
|
||||||
|
<div>
|
||||||
|
<h4>Acil kredi mi lazım?</h4>
|
||||||
|
<p>Kredi fırsatlarını hemen incele.</p>
|
||||||
|
</div>
|
||||||
|
<span class="lt-tag">Yeni</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="lt-section-title">İlan Özellikleri</h2>
|
||||||
|
<div class="lt-features">
|
||||||
|
<div class="lt-feature-row">
|
||||||
|
<div class="lt-f-item"><span>İlan No</span><strong>{{ $listing->id }}</strong></div>
|
||||||
|
<div class="lt-f-item"><span>Marka</span><strong>{{ $listing->category?->name ?? '-' }}</strong></div>
|
||||||
|
</div>
|
||||||
|
<div class="lt-feature-row">
|
||||||
|
<div class="lt-f-item"><span>Model</span><strong>{{ $listing->slug ?? '-' }}</strong></div>
|
||||||
|
<div class="lt-f-item"><span>Yayın Tarihi</span><strong>{{ $publishedAt ?? '-' }}</strong></div>
|
||||||
|
</div>
|
||||||
|
@foreach(($presentableCustomFields ?? []) as $chunk)
|
||||||
|
<div class="lt-feature-row">
|
||||||
|
<div class="lt-f-item"><span>{{ $chunk['label'] ?? '-' }}</span><strong>{{ $chunk['value'] ?? '-' }}</strong></div>
|
||||||
|
<div class="lt-f-item"><span>Konum</span><strong>{{ $locationLabel !== '' ? $locationLabel : '-' }}</strong></div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="lt-card lt-side-card">
|
||||||
|
<div class="lt-seller-head">
|
||||||
|
<div class="lt-avatar">{{ $sellerInitial !== '' ? $sellerInitial : 'S' }}</div>
|
||||||
|
<div>
|
||||||
|
<p class="lt-seller-name">{{ $sellerName }}</p>
|
||||||
|
<div class="lt-seller-meta">{{ $sellerMemberText }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lt-actions">
|
||||||
|
<div class="lt-row-2">
|
||||||
|
@if($listing->user && auth()->check() && (int) auth()->id() !== (int) $listing->user_id)
|
||||||
|
@if($existingConversationId)
|
||||||
|
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="lt-btn">Sohbet</a>
|
||||||
|
@else
|
||||||
|
<form method="POST" action="{{ route('conversations.start', $listing) }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="lt-btn" style="width:100%;">Sohbet</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
@if(auth()->check())
|
||||||
|
<button type="button" class="lt-btn" disabled>Sohbet</button>
|
||||||
|
@else
|
||||||
|
<a href="{{ route('login') }}" class="lt-btn">Sohbet</a>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($listing->contact_phone)
|
||||||
|
<a href="tel:{{ preg_replace('/\s+/', '', (string) $listing->contact_phone) }}" class="lt-btn lt-btn-soft">Ara</a>
|
||||||
|
@else
|
||||||
|
<button type="button" class="lt-btn lt-btn-soft" disabled>Ara</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($listing->user && auth()->check() && (int) auth()->id() !== (int) $listing->user_id)
|
||||||
|
@if($existingConversationId)
|
||||||
|
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="lt-btn lt-btn-main">Teklif Yap</a>
|
||||||
|
@else
|
||||||
|
<form method="POST" action="{{ route('conversations.start', $listing) }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="lt-btn lt-btn-main">Teklif Yap</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<button type="button" class="lt-btn lt-btn-main" disabled>Teklif Yap</button>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="lt-row-2">
|
||||||
|
<a href="#" class="lt-btn lt-btn-outline">Harita</a>
|
||||||
|
@if($listing->user)
|
||||||
|
<a href="{{ route('favorites.index', ['tab' => 'sellers']) }}" class="lt-btn lt-btn-outline">Satıcı Profili</a>
|
||||||
|
@else
|
||||||
|
<a href="#" class="lt-btn lt-btn-outline">Satıcı Profili</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="#" class="lt-report">İlan ile ilgili şikayetim var</a>
|
||||||
|
<div class="lt-policy">İade ve Geri Ödeme Politikası</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="lt-related">
|
||||||
|
<div class="lt-related-head">
|
||||||
|
<h3 class="lt-related-title">İlgini çekebilecek diğer ilanlar</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lt-scroll-wrap" data-theme-scroll>
|
||||||
|
<button type="button" class="lt-scroll-btn prev" data-theme-scroll-prev aria-label="Önceki">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m15 18-6-6 6-6"/></svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="lt-scroll-track" data-theme-scroll-track>
|
||||||
|
@foreach(($relatedListings ?? collect()) as $related)
|
||||||
|
@php
|
||||||
|
$relatedImage = $related->getFirstMediaUrl('listing-images');
|
||||||
|
if (! $relatedImage && is_array($related->images ?? null)) {
|
||||||
|
$relatedImage = collect($related->images)->first();
|
||||||
|
}
|
||||||
|
$relatedPrice = ! is_null($related->price)
|
||||||
|
? (((float) $related->price > 0) ? number_format((float) $related->price, 0, ',', '.').' '.($related->currency ?: 'TL') : 'Ücretsiz')
|
||||||
|
: 'Fiyat sorunuz';
|
||||||
|
@endphp
|
||||||
|
<a href="{{ route('listings.show', $related) }}" class="lt-rel-card">
|
||||||
|
<div class="lt-rel-photo">
|
||||||
|
@if($relatedImage)
|
||||||
|
<img src="{{ $relatedImage }}" alt="{{ $related->title }}">
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="lt-rel-body">
|
||||||
|
<div class="lt-rel-price">{{ $relatedPrice }}</div>
|
||||||
|
<div class="lt-rel-title">{{ $related->title }}</div>
|
||||||
|
<div class="lt-rel-city">{{ trim(collect([$related->city, $related->country])->filter()->implode(', ')) }}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="lt-scroll-btn next" data-theme-scroll-next aria-label="Sonraki">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lt-pill-wrap">
|
||||||
|
<h4 class="lt-pill-title">Daha fazla kategori</h4>
|
||||||
|
<div class="lt-pills">
|
||||||
|
@foreach(($themePillCategories ?? collect()) as $pillCategory)
|
||||||
|
<a href="{{ route('listings.index', ['category' => $pillCategory->id]) }}" class="lt-pill">{{ $pillCategory->name }}</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
document.querySelectorAll('[data-gallery]').forEach((galleryRoot) => {
|
||||||
|
const mainImage = galleryRoot.querySelector('[data-gallery-main]');
|
||||||
|
const thumbButtons = Array.from(galleryRoot.querySelectorAll('[data-gallery-thumb]'));
|
||||||
|
const prevButton = galleryRoot.querySelector('[data-gallery-prev]');
|
||||||
|
const nextButton = galleryRoot.querySelector('[data-gallery-next]');
|
||||||
|
|
||||||
|
if (!mainImage || thumbButtons.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeIndex = thumbButtons.findIndex((button) => button.classList.contains('is-active'));
|
||||||
|
if (activeIndex < 0) {
|
||||||
|
activeIndex = 0;
|
||||||
|
thumbButtons[0].classList.add('is-active');
|
||||||
|
}
|
||||||
|
|
||||||
|
const activate = (index) => {
|
||||||
|
if (index < 0 || index >= thumbButtons.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeIndex = index;
|
||||||
|
const src = thumbButtons[index].dataset.gallerySrc;
|
||||||
|
if (src) {
|
||||||
|
mainImage.src = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbButtons.forEach((button, buttonIndex) => {
|
||||||
|
button.classList.toggle('is-active', buttonIndex === activeIndex);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
thumbButtons.forEach((button, index) => {
|
||||||
|
button.addEventListener('click', () => activate(index));
|
||||||
|
});
|
||||||
|
|
||||||
|
prevButton?.addEventListener('click', () => {
|
||||||
|
activate((activeIndex - 1 + thumbButtons.length) % thumbButtons.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
nextButton?.addEventListener('click', () => {
|
||||||
|
activate((activeIndex + 1) % thumbButtons.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-theme-scroll]').forEach((scrollRoot) => {
|
||||||
|
const track = scrollRoot.querySelector('[data-theme-scroll-track]');
|
||||||
|
const prev = scrollRoot.querySelector('[data-theme-scroll-prev]');
|
||||||
|
const next = scrollRoot.querySelector('[data-theme-scroll-next]');
|
||||||
|
|
||||||
|
if (!track) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = () => Math.max(280, Math.floor(track.clientWidth * 0.72));
|
||||||
|
|
||||||
|
prev?.addEventListener('click', () => {
|
||||||
|
track.scrollBy({ left: -amount(), behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
|
||||||
|
next?.addEventListener('click', () => {
|
||||||
|
track.scrollBy({ left: amount(), behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
@endsection
|
||||||
@ -2,9 +2,17 @@
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/locations/cities/{country}', function(\Modules\Location\Models\Country $country) {
|
Route::get('/locations/cities/{country}', function(\Modules\Location\Models\Country $country) {
|
||||||
|
$activeCities = $country->cities()
|
||||||
|
->where('is_active', true)
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'country_id']);
|
||||||
|
|
||||||
|
if ($activeCities->isNotEmpty()) {
|
||||||
|
return response()->json($activeCities);
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json(
|
return response()->json(
|
||||||
$country->cities()
|
$country->cities()
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'name', 'country_id'])
|
->get(['id', 'name', 'country_id'])
|
||||||
);
|
);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ use App\Support\QuickListingCategorySuggester;
|
|||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\Page;
|
use Filament\Resources\Pages\Page;
|
||||||
|
use Filament\Support\Enums\Width;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@ -33,6 +34,7 @@ class QuickCreateListing extends Page
|
|||||||
protected static ?string $title = 'AI ile Hızlı İlan Ver';
|
protected static ?string $title = 'AI ile Hızlı İlan Ver';
|
||||||
protected static ?string $slug = 'quick-create';
|
protected static ?string $slug = 'quick-create';
|
||||||
protected static bool $shouldRegisterNavigation = false;
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
protected Width | string | null $maxContentWidth = Width::Full;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array<int, TemporaryUploadedFile>
|
* @var array<int, TemporaryUploadedFile>
|
||||||
|
|||||||
@ -3,10 +3,8 @@ namespace Modules\Partner\Providers;
|
|||||||
|
|
||||||
use A909M\FilamentStateFusion\FilamentStateFusionPlugin;
|
use A909M\FilamentStateFusion\FilamentStateFusionPlugin;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Settings\GeneralSettings;
|
|
||||||
use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin;
|
use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin;
|
||||||
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
|
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
|
||||||
use DutchCodingCompany\FilamentSocialite\Provider;
|
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
use Filament\Http\Middleware\AuthenticateSession;
|
use Filament\Http\Middleware\AuthenticateSession;
|
||||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||||
@ -25,8 +23,8 @@ use Illuminate\Support\Str;
|
|||||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
use Jeffgreco13\FilamentBreezy\BreezyCore;
|
use Jeffgreco13\FilamentBreezy\BreezyCore;
|
||||||
use Laravel\Socialite\Contracts\User as SocialiteUserContract;
|
use Laravel\Socialite\Contracts\User as SocialiteUserContract;
|
||||||
|
use Modules\Partner\Support\Filament\SocialiteProviderResolver;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Role;
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
class PartnerPanelProvider extends PanelProvider
|
class PartnerPanelProvider extends PanelProvider
|
||||||
{
|
{
|
||||||
@ -79,7 +77,7 @@ class PartnerPanelProvider extends PanelProvider
|
|||||||
private static function socialitePlugin(): FilamentSocialitePlugin
|
private static function socialitePlugin(): FilamentSocialitePlugin
|
||||||
{
|
{
|
||||||
return FilamentSocialitePlugin::make()
|
return FilamentSocialitePlugin::make()
|
||||||
->providers(self::socialiteProviders())
|
->providers(SocialiteProviderResolver::providers())
|
||||||
->registration(true)
|
->registration(true)
|
||||||
->resolveUserUsing(function (string $provider, SocialiteUserContract $oauthUser): ?User {
|
->resolveUserUsing(function (string $provider, SocialiteUserContract $oauthUser): ?User {
|
||||||
if (! filled($oauthUser->getEmail())) {
|
if (! filled($oauthUser->getEmail())) {
|
||||||
@ -111,60 +109,6 @@ class PartnerPanelProvider extends PanelProvider
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, Provider>
|
|
||||||
*/
|
|
||||||
private static function socialiteProviders(): array
|
|
||||||
{
|
|
||||||
$providers = [];
|
|
||||||
|
|
||||||
if (self::providerEnabled('google')) {
|
|
||||||
$providers[] = Provider::make('google')
|
|
||||||
->label('Google')
|
|
||||||
->icon('heroicon-o-globe-alt')
|
|
||||||
->color(Color::hex('#4285F4'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self::providerEnabled('facebook')) {
|
|
||||||
$providers[] = Provider::make('facebook')
|
|
||||||
->label('Facebook')
|
|
||||||
->icon('heroicon-o-users')
|
|
||||||
->color(Color::hex('#1877F2'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self::providerEnabled('apple')) {
|
|
||||||
$providers[] = Provider::make('apple')
|
|
||||||
->label('Apple')
|
|
||||||
->icon('heroicon-o-device-phone-mobile')
|
|
||||||
->color(Color::Gray)
|
|
||||||
->stateless(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $providers;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function providerEnabled(string $provider): bool
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$settings = app(GeneralSettings::class);
|
|
||||||
|
|
||||||
$enabled = match ($provider) {
|
|
||||||
'google' => (bool) $settings->enable_google_login,
|
|
||||||
'facebook' => (bool) $settings->enable_facebook_login,
|
|
||||||
'apple' => (bool) $settings->enable_apple_login,
|
|
||||||
default => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return $enabled
|
|
||||||
&& filled(config("services.{$provider}.client_id"))
|
|
||||||
&& filled(config("services.{$provider}.client_secret"));
|
|
||||||
} catch (Throwable) {
|
|
||||||
return (bool) config("services.{$provider}.enabled", false)
|
|
||||||
&& filled(config("services.{$provider}.client_id"))
|
|
||||||
&& filled(config("services.{$provider}.client_secret"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function partnerCreateListingUrl(): ?string
|
private static function partnerCreateListingUrl(): ?string
|
||||||
{
|
{
|
||||||
$partner = User::query()->where('email', 'b@b.com')->first();
|
$partner = User::query()->where('email', 'b@b.com')->first();
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Partner\Support\Filament;
|
||||||
|
|
||||||
|
use App\Settings\GeneralSettings;
|
||||||
|
use DutchCodingCompany\FilamentSocialite\Provider;
|
||||||
|
use Filament\Support\Colors\Color;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class SocialiteProviderResolver
|
||||||
|
{
|
||||||
|
public static function providers(): array
|
||||||
|
{
|
||||||
|
$providers = [];
|
||||||
|
|
||||||
|
if (self::enabled('google')) {
|
||||||
|
$providers[] = Provider::make('google')
|
||||||
|
->label('Google')
|
||||||
|
->icon('heroicon-o-globe-alt')
|
||||||
|
->color(Color::hex('#4285F4'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::enabled('facebook')) {
|
||||||
|
$providers[] = Provider::make('facebook')
|
||||||
|
->label('Facebook')
|
||||||
|
->icon('heroicon-o-users')
|
||||||
|
->color(Color::hex('#1877F2'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::enabled('apple')) {
|
||||||
|
$providers[] = Provider::make('apple')
|
||||||
|
->label('Apple')
|
||||||
|
->icon('heroicon-o-device-phone-mobile')
|
||||||
|
->color(Color::Gray)
|
||||||
|
->stateless(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function enabled(string $provider): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$settings = app(GeneralSettings::class);
|
||||||
|
|
||||||
|
$enabled = match ($provider) {
|
||||||
|
'google' => (bool) $settings->enable_google_login,
|
||||||
|
'facebook' => (bool) $settings->enable_facebook_login,
|
||||||
|
'apple' => (bool) $settings->enable_apple_login,
|
||||||
|
default => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return $enabled
|
||||||
|
&& filled(config("services.{$provider}.client_id"))
|
||||||
|
&& filled(config("services.{$provider}.client_secret"));
|
||||||
|
} catch (Throwable) {
|
||||||
|
return (bool) config("services.{$provider}.enabled", false)
|
||||||
|
&& filled(config("services.{$provider}.client_id"))
|
||||||
|
&& filled(config("services.{$provider}.client_secret"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Modules/Theme/Providers/ThemeServiceProvider.php
Normal file
18
Modules/Theme/Providers/ThemeServiceProvider.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Theme\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Modules\Theme\Support\ThemeManager;
|
||||||
|
|
||||||
|
class ThemeServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
$this->mergeConfigFrom(module_path('Theme', 'config/theme.php'), 'theme');
|
||||||
|
|
||||||
|
$this->app->singleton(ThemeManager::class, function ($app): ThemeManager {
|
||||||
|
return new ThemeManager($app['config']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Modules/Theme/Support/ThemeManager.php
Normal file
50
Modules/Theme/Support/ThemeManager.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Theme\Support;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Config\Repository;
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class ThemeManager
|
||||||
|
{
|
||||||
|
public function __construct(private Repository $config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activeTheme(string $module): string
|
||||||
|
{
|
||||||
|
$moduleKey = Str::lower($module);
|
||||||
|
$moduleSpecific = $this->config->get("theme.modules.{$moduleKey}");
|
||||||
|
|
||||||
|
if (is_string($moduleSpecific) && $moduleSpecific !== '') {
|
||||||
|
return Str::lower($moduleSpecific);
|
||||||
|
}
|
||||||
|
|
||||||
|
$global = $this->config->get('theme.active', 'default');
|
||||||
|
|
||||||
|
if (! is_string($global) || $global === '') {
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Str::lower($global);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(string $module, string $name): string
|
||||||
|
{
|
||||||
|
$moduleKey = Str::lower($module);
|
||||||
|
$activeTheme = $this->activeTheme($moduleKey);
|
||||||
|
|
||||||
|
$primary = "{$moduleKey}::themes.{$activeTheme}.{$name}";
|
||||||
|
if (View::exists($primary)) {
|
||||||
|
return $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultTheme = "{$moduleKey}::themes.default.{$name}";
|
||||||
|
if (View::exists($defaultTheme)) {
|
||||||
|
return $defaultTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{$moduleKey}::{$name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Modules/Theme/config/theme.php
Normal file
9
Modules/Theme/config/theme.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'active' => env('OC_THEME', 'otoplus'),
|
||||||
|
'modules' => [
|
||||||
|
'listing' => env('OC_THEME_LISTING', 'otoplus'),
|
||||||
|
'category' => env('OC_THEME_CATEGORY', 'otoplus'),
|
||||||
|
],
|
||||||
|
];
|
||||||
12
Modules/Theme/module.json
Normal file
12
Modules/Theme/module.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Theme",
|
||||||
|
"alias": "theme",
|
||||||
|
"description": "Modular theme selection and themed view resolution",
|
||||||
|
"keywords": [],
|
||||||
|
"priority": 0,
|
||||||
|
"providers": [
|
||||||
|
"Modules\\Theme\\Providers\\ThemeServiceProvider"
|
||||||
|
],
|
||||||
|
"aliases": {},
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
@ -76,7 +76,7 @@ class FavoriteController extends Controller
|
|||||||
'listing:id,title,price,currency,user_id',
|
'listing:id,title,price,currency,user_id',
|
||||||
'buyer:id,name',
|
'buyer:id,name',
|
||||||
'seller:id,name',
|
'seller:id,name',
|
||||||
'lastMessage:id,conversation_id,sender_id,body,created_at',
|
'lastMessage',
|
||||||
'lastMessage.sender:id,name',
|
'lastMessage.sender:id,name',
|
||||||
])
|
])
|
||||||
->withCount([
|
->withCount([
|
||||||
|
|||||||
@ -80,7 +80,7 @@ class PanelController extends Controller
|
|||||||
'listing:id,title,price,currency,user_id',
|
'listing:id,title,price,currency,user_id',
|
||||||
'buyer:id,name',
|
'buyer:id,name',
|
||||||
'seller:id,name',
|
'seller:id,name',
|
||||||
'lastMessage:id,conversation_id,sender_id,body,created_at',
|
'lastMessage',
|
||||||
])
|
])
|
||||||
->withCount([
|
->withCount([
|
||||||
'messages as unread_count' => fn ($query) => $query
|
'messages as unread_count' => fn ($query) => $query
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
use App\Support\QuickListingCategorySuggester;
|
use App\Support\QuickListingCategorySuggester;
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@ -171,10 +170,10 @@ class PanelQuickListingForm extends Component
|
|||||||
$this->loadListingCustomFields();
|
$this->loadListingCustomFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function publishListing(): ?RedirectResponse
|
public function publishListing(): void
|
||||||
{
|
{
|
||||||
if ($this->isPublishing) {
|
if ($this->isPublishing) {
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->isPublishing = true;
|
$this->isPublishing = true;
|
||||||
@ -191,13 +190,13 @@ class PanelQuickListingForm extends Component
|
|||||||
$this->isPublishing = false;
|
$this->isPublishing = false;
|
||||||
session()->flash('error', 'İlan oluşturulamadı. Lütfen tekrar deneyin.');
|
session()->flash('error', 'İlan oluşturulamadı. Lütfen tekrar deneyin.');
|
||||||
|
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->isPublishing = false;
|
$this->isPublishing = false;
|
||||||
session()->flash('success', 'İlan başarıyla oluşturuldu.');
|
session()->flash('success', 'İlan başarıyla oluşturuldu.');
|
||||||
|
|
||||||
return redirect()->route('panel.listings.index');
|
$this->redirectRoute('panel.listings.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRootCategoriesProperty(): array
|
public function getRootCategoriesProperty(): array
|
||||||
|
|||||||
@ -44,7 +44,15 @@ class Conversation extends Model
|
|||||||
|
|
||||||
public function lastMessage()
|
public function lastMessage()
|
||||||
{
|
{
|
||||||
return $this->hasOne(ConversationMessage::class)->latestOfMany();
|
return $this->hasOne(ConversationMessage::class)
|
||||||
|
->latestOfMany()
|
||||||
|
->select([
|
||||||
|
'conversation_messages.id',
|
||||||
|
'conversation_messages.conversation_id',
|
||||||
|
'conversation_messages.sender_id',
|
||||||
|
'conversation_messages.body',
|
||||||
|
'conversation_messages.created_at',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopeForUser(Builder $query, int $userId): Builder
|
public function scopeForUser(Builder $query, int $userId): Builder
|
||||||
@ -55,4 +63,14 @@ class Conversation extends Model
|
|||||||
->orWhere('seller_id', $userId);
|
->orWhere('seller_id', $userId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function buyerListingConversationId(int $listingId, int $buyerId): ?int
|
||||||
|
{
|
||||||
|
$value = static::query()
|
||||||
|
->where('listing_id', $listingId)
|
||||||
|
->where('buyer_id', $buyerId)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
return is_null($value) ? null : (int) $value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Gate;
|
|||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
|
use Modules\Category\Models\Category;
|
||||||
use Modules\Location\Models\Country;
|
use Modules\Location\Models\Country;
|
||||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
@ -193,6 +194,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
});
|
});
|
||||||
|
|
||||||
$headerLocationCountries = [];
|
$headerLocationCountries = [];
|
||||||
|
$headerNavCategories = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Schema::hasTable('countries') && Schema::hasTable('cities')) {
|
if (Schema::hasTable('countries') && Schema::hasTable('cities')) {
|
||||||
@ -214,8 +216,29 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$headerLocationCountries = [];
|
$headerLocationCountries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (Schema::hasTable('categories')) {
|
||||||
|
$headerNavCategories = Category::query()
|
||||||
|
->where('is_active', true)
|
||||||
|
->whereNull('parent_id')
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('name')
|
||||||
|
->limit(8)
|
||||||
|
->get(['id', 'name'])
|
||||||
|
->map(fn (Category $category): array => [
|
||||||
|
'id' => (int) $category->id,
|
||||||
|
'name' => (string) $category->name,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
$headerNavCategories = [];
|
||||||
|
}
|
||||||
|
|
||||||
View::share('generalSettings', $generalSettings);
|
View::share('generalSettings', $generalSettings);
|
||||||
View::share('headerLocationCountries', $headerLocationCountries);
|
View::share('headerLocationCountries', $headerLocationCountries);
|
||||||
|
View::share('headerNavCategories', $headerNavCategories);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeCurrencies(array $currencies): array
|
private function normalizeCurrencies(array $currencies): array
|
||||||
|
|||||||
@ -4,5 +4,6 @@
|
|||||||
"Location": true,
|
"Location": true,
|
||||||
"Profile": true,
|
"Profile": true,
|
||||||
"Admin": true,
|
"Admin": true,
|
||||||
"Partner": false
|
"Partner": false,
|
||||||
|
"Theme": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -27,13 +27,11 @@
|
|||||||
'ru' => 'Русский',
|
'ru' => 'Русский',
|
||||||
'ja' => '日本語',
|
'ja' => '日本語',
|
||||||
];
|
];
|
||||||
$isHomePage = request()->routeIs('home');
|
$headerCategories = collect($headerNavCategories ?? [])->values();
|
||||||
$isSimplePage = trim($__env->yieldContent('simple_page')) === '1';
|
|
||||||
$homeHeaderCategories = isset($categories) ? collect($categories)->take(8) : collect();
|
|
||||||
$locationCountries = collect($headerLocationCountries ?? [])->values();
|
$locationCountries = collect($headerLocationCountries ?? [])->values();
|
||||||
$defaultCountryIso2 = strtoupper((string) config('app.default_country_iso2', 'TR'));
|
$defaultCountryIso2 = strtoupper((string) config('app.default_country_iso2', 'TR'));
|
||||||
$citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities')
|
$citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities')
|
||||||
? route('locations.cities', ['country' => '__COUNTRY__'])
|
? route('locations.cities', ['country' => '__COUNTRY__'], false)
|
||||||
: '';
|
: '';
|
||||||
@endphp
|
@endphp
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@ -258,7 +256,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if(!$isSimplePage && $isHomePage && $homeHeaderCategories->isNotEmpty())
|
|
||||||
<div class="mt-4 border-t border-slate-200 pt-3 overflow-x-auto">
|
<div class="mt-4 border-t border-slate-200 pt-3 overflow-x-auto">
|
||||||
<div class="flex items-center gap-2 min-w-max pb-1">
|
<div class="flex items-center gap-2 min-w-max pb-1">
|
||||||
<a href="{{ route('categories.index') }}" class="chip-btn inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-100 transition">
|
<a href="{{ route('categories.index') }}" class="chip-btn inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-100 transition">
|
||||||
@ -267,20 +264,16 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Tüm Kategoriler
|
Tüm Kategoriler
|
||||||
</a>
|
</a>
|
||||||
@foreach($homeHeaderCategories as $headerCategory)
|
@forelse($headerCategories as $headerCategory)
|
||||||
<a href="{{ route('categories.show', $headerCategory) }}" class="px-4 py-2.5 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-100 transition whitespace-nowrap">
|
<a href="{{ route('categories.show', ['category' => $headerCategory['id']]) }}" class="px-4 py-2.5 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-100 transition whitespace-nowrap">
|
||||||
{{ $headerCategory->name }}
|
{{ $headerCategory['name'] }}
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@empty
|
||||||
|
<a href="{{ route('home') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.home') }}</a>
|
||||||
|
<a href="{{ route('listings.index') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.listings') }}</a>
|
||||||
|
@endforelse
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@elseif(! $isSimplePage && ! $isHomePage)
|
|
||||||
<div class="mt-3 flex items-center gap-2 text-sm overflow-x-auto pb-1">
|
|
||||||
<a href="{{ route('home') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.home') }}</a>
|
|
||||||
<a href="{{ route('categories.index') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.categories') }}</a>
|
|
||||||
<a href="{{ route('listings.index') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.listings') }}</a>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@if(session('success'))
|
@if(session('success'))
|
||||||
@ -412,6 +405,27 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (root, countryId, selectedCityId = null, selectedCityName = null) => {
|
const loadCities = async (root, countryId, selectedCityId = null, selectedCityName = null) => {
|
||||||
const citySelect = root.querySelector('[data-location-city]');
|
const citySelect = root.querySelector('[data-location-city]');
|
||||||
const countrySelect = root.querySelector('[data-location-country]');
|
const countrySelect = root.querySelector('[data-location-country]');
|
||||||
@ -432,20 +446,32 @@
|
|||||||
citySelect.innerHTML = '<option value="">Şehir yükleniyor...</option>';
|
citySelect.innerHTML = '<option value="">Şehir yükleniyor...</option>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(template.replace('__COUNTRY__', encodeURIComponent(String(countryId))), {
|
const primaryUrl = template.replace('__COUNTRY__', encodeURIComponent(String(countryId)));
|
||||||
headers: {
|
let cityOptions;
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
try {
|
||||||
throw new Error('city_fetch_failed');
|
cityOptions = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
cityOptions = await fetchCityOptions(fallbackUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cities = await response.json();
|
|
||||||
const cityOptions = Array.isArray(cities) ? cities : [];
|
|
||||||
|
|
||||||
citySelect.innerHTML = '<option value="">Şehir seç</option>';
|
citySelect.innerHTML = '<option value="">Şehir seç</option>';
|
||||||
|
|
||||||
cityOptions.forEach((city) => {
|
cityOptions.forEach((city) => {
|
||||||
|
|||||||
@ -5,9 +5,5 @@
|
|||||||
@section('simple_page', '1')
|
@section('simple_page', '1')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
<livewire:panel-quick-listing-form />
|
||||||
<section class="bg-white border border-slate-200 rounded-xl p-0 overflow-hidden">
|
|
||||||
<livewire:panel-quick-listing-form />
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1333
resources/views/partials/quick-create/form.blade.php
Normal file
1333
resources/views/partials/quick-create/form.blade.php
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user