diff --git a/.env.example b/.env.example index 5f8ec325a..b44d2eec0 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,9 @@ APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://localhost:8000 +OC_THEME=otoplus +OC_THEME_LISTING=otoplus +OC_THEME_CATEGORY=otoplus APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/Modules/Category/Http/Controllers/CategoryController.php b/Modules/Category/Http/Controllers/CategoryController.php index a4ded2e7e..7f016191b 100644 --- a/Modules/Category/Http/Controllers/CategoryController.php +++ b/Modules/Category/Http/Controllers/CategoryController.php @@ -1,21 +1,34 @@ with('children')->where('is_active', true)->get(); - return view('category::index', compact('categories')); + $categories = Category::rootTreeWithActiveChildren(); + + return view($this->themes->view('category', 'index'), compact('categories')); } public function show(Category $category) { - $listings = $category->listings()->where('status', 'active')->paginate(12); - return view('category::show', compact('category', 'listings')); + $category->loadMissing([ + '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')); } } diff --git a/Modules/Category/Models/Category.php b/Modules/Category/Models/Category.php index 0ef96af8c..6b2b2870f 100644 --- a/Modules/Category/Models/Category.php +++ b/Modules/Category/Models/Category.php @@ -1,9 +1,11 @@ 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 { return $this->hasMany(\Modules\Listing\Models\ListingCustomField::class); } + + public function activeListings(): HasMany + { + return $this->hasMany(\Modules\Listing\Models\Listing::class)->where('status', 'active'); + } } diff --git a/Modules/Category/resources/views/themes/README.md b/Modules/Category/resources/views/themes/README.md new file mode 100644 index 000000000..835cb8a57 --- /dev/null +++ b/Modules/Category/resources/views/themes/README.md @@ -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}`. diff --git a/Modules/Category/resources/views/themes/default/index.blade.php b/Modules/Category/resources/views/themes/default/index.blade.php new file mode 100644 index 000000000..e7c719c45 --- /dev/null +++ b/Modules/Category/resources/views/themes/default/index.blade.php @@ -0,0 +1,15 @@ +@extends('app::layouts.app') +@section('content') +
+

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

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

{{ $category->name }}

+

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

+
+ @endforeach +
+
+@endsection diff --git a/Modules/Category/resources/views/themes/default/show.blade.php b/Modules/Category/resources/views/themes/default/show.blade.php new file mode 100644 index 000000000..e827be93e --- /dev/null +++ b/Modules/Category/resources/views/themes/default/show.blade.php @@ -0,0 +1,33 @@ +@extends('app::layouts.app') +@section('content') +
+
+

{{ $category->icon ?? '' }} {{ $category->name }}

+ @if($category->description)

{{ $category->description }}

@endif +
+ @if($category->children->count()) +
+ @foreach($category->children as $child) + +

{{ $child->name }}

+
+ @endforeach +
+ @endif +

Listings in {{ $category->name }}

+
+ @forelse($listings as $listing) +
+
+

{{ $listing->title }}

+

{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}

+ View → +
+
+ @empty +

No listings in this category yet.

+ @endforelse +
+
{{ $listings->links() }}
+
+@endsection diff --git a/Modules/Category/resources/views/themes/otoplus/index.blade.php b/Modules/Category/resources/views/themes/otoplus/index.blade.php new file mode 100644 index 000000000..e7c719c45 --- /dev/null +++ b/Modules/Category/resources/views/themes/otoplus/index.blade.php @@ -0,0 +1,15 @@ +@extends('app::layouts.app') +@section('content') +
+

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

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

{{ $category->name }}

+

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

+
+ @endforeach +
+
+@endsection diff --git a/Modules/Category/resources/views/themes/otoplus/show.blade.php b/Modules/Category/resources/views/themes/otoplus/show.blade.php new file mode 100644 index 000000000..e827be93e --- /dev/null +++ b/Modules/Category/resources/views/themes/otoplus/show.blade.php @@ -0,0 +1,33 @@ +@extends('app::layouts.app') +@section('content') +
+
+

{{ $category->icon ?? '' }} {{ $category->name }}

+ @if($category->description)

{{ $category->description }}

@endif +
+ @if($category->children->count()) +
+ @foreach($category->children as $child) + +

{{ $child->name }}

+
+ @endforeach +
+ @endif +

Listings in {{ $category->name }}

+
+ @forelse($listings as $listing) +
+
+

{{ $listing->title }}

+

{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}

+ View → +
+
+ @empty +

No listings in this category yet.

+ @endforelse +
+
{{ $listings->links() }}
+
+@endsection diff --git a/Modules/Listing/Database/migrations/2026_03_03_200000_create_listing_custom_fields_table_and_add_custom_fields_to_listings_table.php b/Modules/Listing/Database/migrations/2024_01_01_000004_create_listing_custom_fields_table.php similarity index 78% rename from Modules/Listing/Database/migrations/2026_03_03_200000_create_listing_custom_fields_table_and_add_custom_fields_to_listings_table.php rename to Modules/Listing/Database/migrations/2024_01_01_000004_create_listing_custom_fields_table.php index 2e872fcc8..f83dbf8c6 100644 --- a/Modules/Listing/Database/migrations/2026_03_03_200000_create_listing_custom_fields_table_and_add_custom_fields_to_listings_table.php +++ b/Modules/Listing/Database/migrations/2024_01_01_000004_create_listing_custom_fields_table.php @@ -22,18 +22,10 @@ return new class extends Migration $table->unsignedInteger('sort_order')->default(0); $table->timestamps(); }); - - Schema::table('listings', function (Blueprint $table): void { - $table->json('custom_fields')->nullable()->after('images'); - }); } public function down(): void { - Schema::table('listings', function (Blueprint $table): void { - $table->dropColumn('custom_fields'); - }); - Schema::dropIfExists('listing_custom_fields'); } }; diff --git a/Modules/Listing/Database/migrations/2026_03_03_140200_add_coordinates_to_listings_table.php b/Modules/Listing/Database/migrations/2026_03_03_140200_add_coordinates_to_listings_table.php deleted file mode 100644 index 64d521c89..000000000 --- a/Modules/Listing/Database/migrations/2026_03_03_140200_add_coordinates_to_listings_table.php +++ /dev/null @@ -1,34 +0,0 @@ -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'); - } - }); - } -}; diff --git a/Modules/Listing/Database/migrations/2026_03_03_233000_add_view_count_to_listings_table.php b/Modules/Listing/Database/migrations/2026_03_03_233000_add_view_count_to_listings_table.php deleted file mode 100644 index a20e3ec60..000000000 --- a/Modules/Listing/Database/migrations/2026_03_03_233000_add_view_count_to_listings_table.php +++ /dev/null @@ -1,26 +0,0 @@ -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'); - } - }); - } -}; diff --git a/Modules/Listing/Http/Controllers/ListingController.php b/Modules/Listing/Http/Controllers/ListingController.php index 10ab84cd3..6e99d97da 100644 --- a/Modules/Listing/Http/Controllers/ListingController.php +++ b/Modules/Listing/Http/Controllers/ListingController.php @@ -4,39 +4,106 @@ namespace Modules\Listing\Http\Controllers; use App\Http\Controllers\Controller; use App\Models\Conversation; use App\Models\FavoriteSearch; +use Illuminate\Support\Carbon; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Schema; +use Modules\Location\Models\City; +use Modules\Location\Models\Country; use Modules\Category\Models\Category; use Modules\Listing\Models\Listing; use Modules\Listing\Support\ListingCustomFieldSchemaBuilder; +use Modules\Theme\Support\ThemeManager; +use Throwable; class ListingController extends Controller { + public function __construct(private ThemeManager $themes) + { + } + public function index() { $search = trim((string) request('search', '')); + $categoryId = request()->integer('category'); $categoryId = $categoryId > 0 ? $categoryId : null; - $listings = Listing::query() - ->publicFeed() + $countryId = request()->integer('country'); + $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') - ->when($search !== '', function ($query) use ($search): void { - $query->where(function ($searchQuery) use ($search): void { - $searchQuery - ->where('title', 'like', "%{$search}%") - ->orWhere('description', 'like', "%{$search}%") - ->orWhere('city', 'like', "%{$search}%") - ->orWhere('country', 'like', "%{$search}%"); - }); - }) - ->when($categoryId, fn ($query) => $query->where('category_id', $categoryId)) - ->paginate(12) + ->searchTerm($search) + ->forCategory($categoryId) + ->when($selectedCountryName, fn ($query) => $query->where('country', $selectedCountryName)) + ->when($selectedCityName, fn ($query) => $query->where('city', $selectedCityName)) + ->when(! is_null($minPrice), fn ($query) => $query->whereNotNull('price')->where('price', '>=', $minPrice)) + ->when(! is_null($maxPrice), fn ($query) => $query->whereNotNull('price')->where('price', '<=', $maxPrice)); + + $this->applyDateFilter($listingsQuery, $dateFilter); + $this->applySorting($listingsQuery, $sort); + + $listings = $listingsQuery + ->paginate(16) ->withQueryString(); $categories = Category::query() ->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') - ->get(['id', 'name']); + ->get(['id', 'name', 'parent_id']); + + $selectedCategory = $categoryId + ? Category::query()->whereKey($categoryId)->first(['id', 'name']) + : null; $favoriteListingIds = []; $isCurrentSearchSaved = false; @@ -70,10 +137,19 @@ class ListingController extends Controller } } - return view('listing::index', compact( + return view($this->themes->view('listing', 'index'), compact( 'listings', 'search', 'categoryId', + 'countryId', + 'cityId', + 'minPriceInput', + 'maxPriceInput', + 'dateFilter', + 'sort', + 'countries', + 'cities', + 'selectedCategory', 'categories', 'favoriteListingIds', 'isCurrentSearchSaved', @@ -91,11 +167,22 @@ class ListingController extends Controller $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( $listing->category_id ? (int) $listing->category_id : null, $listing->custom_fields ?? [], ); + $gallery = $listing->themeGallery(); + $relatedListings = $listing->relatedSuggestions(12); + $themePillCategories = Category::themePills(10); + $breadcrumbCategories = $listing->category + ? $listing->category->breadcrumbTrail() + : collect(); $isListingFavorited = false; $isSellerFavorited = false; @@ -117,19 +204,23 @@ class ListingController extends Controller } if ($listing->user_id && (int) $listing->user_id !== $userId) { - $existingConversationId = Conversation::query() - ->where('listing_id', $listing->getKey()) - ->where('buyer_id', $userId) - ->value('id'); + $existingConversationId = Conversation::buyerListingConversationId( + (int) $listing->getKey(), + $userId, + ); } } - return view('listing::show', compact( + return view($this->themes->view('listing', 'show'), compact( 'listing', 'isListingFavorited', 'isSellerFavorited', 'presentableCustomFields', 'existingConversationId', + 'gallery', + 'relatedListings', + 'themePillCategories', + 'breadcrumbCategories', )); } @@ -152,4 +243,101 @@ class ListingController extends Controller ->route('panel.listings.create') ->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'), + }; + } } diff --git a/Modules/Listing/Models/Listing.php b/Modules/Listing/Models/Listing.php index 58d90432f..4a88b3ba6 100644 --- a/Modules/Listing/Models/Listing.php +++ b/Modules/Listing/Models/Listing.php @@ -2,9 +2,10 @@ namespace Modules\Listing\Models; use Illuminate\Database\Eloquent\Casts\Attribute; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Collection; use Illuminate\Support\Str; use Modules\Listing\States\ListingStatus; use Modules\Listing\Support\ListingPanelHelper; @@ -76,6 +77,76 @@ class Listing extends Model implements HasMedia ->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 { $baseSlug = Str::slug((string) ($data['title'] ?? 'listing')); diff --git a/Modules/Listing/database/migrations/2024_01_01_000003_create_listings_table.php b/Modules/Listing/database/migrations/2024_01_01_000003_create_listings_table.php index bdd317c3c..62dffcae1 100644 --- a/Modules/Listing/database/migrations/2024_01_01_000003_create_listings_table.php +++ b/Modules/Listing/database/migrations/2024_01_01_000003_create_listings_table.php @@ -7,7 +7,7 @@ return new class extends Migration { public function up(): void { - Schema::create('listings', function (Blueprint $table) { + Schema::create('listings', function (Blueprint $table): void { $table->id(); $table->string('title'); $table->string('slug')->unique(); @@ -18,12 +18,16 @@ return new class extends Migration $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); $table->string('status')->default('active'); $table->json('images')->nullable(); + $table->json('custom_fields')->nullable(); $table->string('contact_phone')->nullable(); $table->string('contact_email')->nullable(); $table->boolean('is_featured')->default(false); + $table->unsignedInteger('view_count')->default(0); $table->timestamp('expires_at')->nullable(); $table->string('city')->nullable(); $table->string('country')->nullable(); + $table->decimal('latitude', 10, 7)->nullable(); + $table->decimal('longitude', 10, 7)->nullable(); $table->timestamps(); }); } diff --git a/Modules/Listing/resources/views/index.blade.php b/Modules/Listing/resources/views/index.blade.php index 150ff189a..ba87d966f 100644 --- a/Modules/Listing/resources/views/index.blade.php +++ b/Modules/Listing/resources/views/index.blade.php @@ -1,117 +1,478 @@ @extends('app::layouts.app') @section('content') -
-
-

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

+@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 -
- @if($search !== '') - - @endif - - - @if($categoryId) - - Sıfırla - - @endif -
-
+ + +
+

{{ $pageTitle }}

+ +
+
-
- @if($listing->is_featured) - Featured - @endif -

{{ $listing->title }}

-

- @if($listing->price) {{ number_format($listing->price, 0) }} {{ $listing->currency }} @else Free @endif -

-

{{ $listing->category?->name ?: 'Kategori yok' }}

-

{{ $listing->city }}, {{ $listing->country }}

-
- View - @auth - @if($listing->user_id && (int) $listing->user_id !== (int) auth()->id()) - @if($conversationId) - - Sohbete Git + + @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 + + {{ $category->name }} + {{ number_format($categoryCount, 0, ',', '.') }} + + + @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 + + {{ $childCategory->name }} + {{ number_format((int) $childCategory->active_listings_count, 0, ',', '.') }} - @else -
- @csrf - -
- @endif - @endif + @endforeach + @endforeach +
+ + +
+ @if($search !== '') + + @endif + @if($categoryId) + + @endif + + +
+

Konum

+
+ @php + $citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities') + ? route('locations.cities', ['country' => '__COUNTRY__'], false) + : ''; + @endphp + + + + + +
+
+ +
+

Fiyat

+
+ + +
+
+ +
+

İlan Tarihi

+
+ + + + +
+
+ +
+ + Temizle + + +
+
+ + +
+
+

+ {{ $activeCategoryName !== '' ? 'İkinci El '.$activeCategoryName.' kategorisinde' : 'İkinci El Araba kategorisinde' }} + {{ number_format($totalListings, 0, ',', '.') }} + ilan bulundu +

+
+ @auth +
+ @csrf + + + +
+ @else + + Arama Kaydet + @endauth + +
+ @if($search !== '') + + @endif + @if($categoryId) + + @endif + @if($countryId) + + @endif + @if($cityId) + + @endif + @if($minPriceInput !== '') + + @endif + @if($maxPriceInput !== '') + + @endif + @if($dateFilter !== 'all') + + @endif + + +
-
- @empty -
- Bu filtreye uygun ilan bulunamadı. -
- @endforelse + + @if($listings->isEmpty()) +
+ Bu filtreye uygun ilan bulunamadı. +
+ @else +
+ @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 +
+
+ @if($listingImage) + + {{ $listing->title }} + + @else + + + + + + @endif + + @if($listing->is_featured) + + Öne Çıkan + + @endif + +
+ @auth +
+ @csrf + +
+ @else + + ♥ + + @endauth +
+
+ +
+ +

+ @if(!is_null($priceValue) && $priceValue > 0) + {{ number_format($priceValue, 0, ',', '.') }} {{ $listing->currency }} + @else + Ücretsiz + @endif +

+

+ {{ $listing->title }} +

+
+ +

+ {{ $listing->category?->name ?: 'Kategori yok' }} +

+ +
+ {{ $locationText !== '' ? $locationText : 'Konum belirtilmedi' }} + {{ $listing->created_at?->format('d.m.Y') }} +
+
+
+ @endforeach +
+ @endif + +
+ {{ $listings->links() }} +
+
-
{{ $listings->links() }}
+ + @endsection diff --git a/Modules/Listing/resources/views/themes/README.md b/Modules/Listing/resources/views/themes/README.md new file mode 100644 index 000000000..7984fb255 --- /dev/null +++ b/Modules/Listing/resources/views/themes/README.md @@ -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`. diff --git a/Modules/Listing/resources/views/themes/default/index.blade.php b/Modules/Listing/resources/views/themes/default/index.blade.php new file mode 100644 index 000000000..ba87d966f --- /dev/null +++ b/Modules/Listing/resources/views/themes/default/index.blade.php @@ -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 + + + +
+

{{ $pageTitle }}

+ +
+ + +
+
+

+ {{ $activeCategoryName !== '' ? 'İkinci El '.$activeCategoryName.' kategorisinde' : 'İkinci El Araba kategorisinde' }} + {{ number_format($totalListings, 0, ',', '.') }} + ilan bulundu +

+
+ @auth +
+ @csrf + + + +
+ @else + + Arama Kaydet + + @endauth + +
+ @if($search !== '') + + @endif + @if($categoryId) + + @endif + @if($countryId) + + @endif + @if($cityId) + + @endif + @if($minPriceInput !== '') + + @endif + @if($maxPriceInput !== '') + + @endif + @if($dateFilter !== 'all') + + @endif + + +
+
+
+ + @if($listings->isEmpty()) +
+ Bu filtreye uygun ilan bulunamadı. +
+ @else +
+ @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 +
+
+ @if($listingImage) + + {{ $listing->title }} + + @else + + + + + + @endif + + @if($listing->is_featured) + + Öne Çıkan + + @endif + +
+ @auth +
+ @csrf + +
+ @else + + ♥ + + @endauth +
+
+ +
+ +

+ @if(!is_null($priceValue) && $priceValue > 0) + {{ number_format($priceValue, 0, ',', '.') }} {{ $listing->currency }} + @else + Ücretsiz + @endif +

+

+ {{ $listing->title }} +

+
+ +

+ {{ $listing->category?->name ?: 'Kategori yok' }} +

+ +
+ {{ $locationText !== '' ? $locationText : 'Konum belirtilmedi' }} + {{ $listing->created_at?->format('d.m.Y') }} +
+
+
+ @endforeach +
+ @endif + +
+ {{ $listings->links() }} +
+
+
+
+ + +@endsection diff --git a/Modules/Listing/resources/views/themes/default/show.blade.php b/Modules/Listing/resources/views/themes/default/show.blade.php new file mode 100644 index 000000000..a219646e4 --- /dev/null +++ b/Modules/Listing/resources/views/themes/default/show.blade.php @@ -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 +
+
+
+
+ +
+
+
+

{{ $displayTitle }}

+ + @if($hasPrice) + @if($priceValue > 0) + {{ number_format($priceValue, 0) }} {{ $listing->currency ?? 'USD' }} + @else + Free + @endif + @else + Price on request + @endif + +
+
+ @auth +
+ @csrf + +
+ @if($listing->user && (int) $listing->user->id !== (int) auth()->id()) +
+ @csrf + +
+ @if($existingConversationId) + + Sohbete Git + + @else +
+ @csrf + +
+ @endif + @endif + @else + + Giriş yap ve favorile + + @endauth +
+

{{ $location !== '' ? $location : 'Location not specified' }}

+

Posted {{ $listing->created_at?->diffForHumans() ?? 'recently' }}

+
+

Description

+

{{ $displayDescription }}

+
+ @if(($presentableCustomFields ?? []) !== []) +
+

İlan Özellikleri

+
+ @foreach($presentableCustomFields as $field) +
+

{{ $field['label'] }}

+

{{ $field['value'] }}

+
+ @endforeach +
+
+ @endif +
+

Contact Seller

+ @if($listing->user) +

Name: {{ $listing->user->name }}

+ @endif + @if($listing->contact_phone) +

Phone: {{ $listing->contact_phone }}

+ @endif + @if($listing->contact_email) +

Email: {{ $listing->contact_email }}

+ @endif + @if(!$listing->contact_phone && !$listing->contact_email) +

No contact details provided.

+ @endif +
+ +
+
+
+
+@endsection diff --git a/Modules/Listing/resources/views/themes/otoplus/index.blade.php b/Modules/Listing/resources/views/themes/otoplus/index.blade.php new file mode 100644 index 000000000..ba87d966f --- /dev/null +++ b/Modules/Listing/resources/views/themes/otoplus/index.blade.php @@ -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 + + + +
+

{{ $pageTitle }}

+ +
+ + +
+
+

+ {{ $activeCategoryName !== '' ? 'İkinci El '.$activeCategoryName.' kategorisinde' : 'İkinci El Araba kategorisinde' }} + {{ number_format($totalListings, 0, ',', '.') }} + ilan bulundu +

+
+ @auth +
+ @csrf + + + +
+ @else + + Arama Kaydet + + @endauth + +
+ @if($search !== '') + + @endif + @if($categoryId) + + @endif + @if($countryId) + + @endif + @if($cityId) + + @endif + @if($minPriceInput !== '') + + @endif + @if($maxPriceInput !== '') + + @endif + @if($dateFilter !== 'all') + + @endif + + +
+
+
+ + @if($listings->isEmpty()) +
+ Bu filtreye uygun ilan bulunamadı. +
+ @else +
+ @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 +
+
+ @if($listingImage) + + {{ $listing->title }} + + @else + + + + + + @endif + + @if($listing->is_featured) + + Öne Çıkan + + @endif + +
+ @auth +
+ @csrf + +
+ @else + + ♥ + + @endauth +
+
+ +
+ +

+ @if(!is_null($priceValue) && $priceValue > 0) + {{ number_format($priceValue, 0, ',', '.') }} {{ $listing->currency }} + @else + Ücretsiz + @endif +

+

+ {{ $listing->title }} +

+
+ +

+ {{ $listing->category?->name ?: 'Kategori yok' }} +

+ +
+ {{ $locationText !== '' ? $locationText : 'Konum belirtilmedi' }} + {{ $listing->created_at?->format('d.m.Y') }} +
+
+
+ @endforeach +
+ @endif + +
+ {{ $listings->links() }} +
+
+
+
+ + +@endsection diff --git a/Modules/Listing/resources/views/themes/otoplus/show.blade.php b/Modules/Listing/resources/views/themes/otoplus/show.blade.php new file mode 100644 index 000000000..907ff6edc --- /dev/null +++ b/Modules/Listing/resources/views/themes/otoplus/show.blade.php @@ -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 + + + +
+ + +
+
+
+ + + @if($galleryImages !== []) +
+ @foreach($galleryImages as $index => $image) + + @endforeach +
+ @endif +
+ +
+
+
+
{{ $priceLabel }}
+
{{ $displayTitle }}
+
+
+
{{ $locationLabel !== '' ? $locationLabel : 'Konum belirtilmedi' }}
+
{{ $publishedAt ?? '-' }}
+
+
+ +
+
+

Acil kredi mi lazım?

+

Kredi fırsatlarını hemen incele.

+
+ Yeni +
+ +

İlan Özellikleri

+
+
+
İlan No{{ $listing->id }}
+
Marka{{ $listing->category?->name ?? '-' }}
+
+
+
Model{{ $listing->slug ?? '-' }}
+
Yayın Tarihi{{ $publishedAt ?? '-' }}
+
+ @foreach(($presentableCustomFields ?? []) as $chunk) +
+
{{ $chunk['label'] ?? '-' }}{{ $chunk['value'] ?? '-' }}
+
Konum{{ $locationLabel !== '' ? $locationLabel : '-' }}
+
+ @endforeach +
+
+
+ + +
+ + +
+ + +@endsection diff --git a/Modules/Location/routes/web.php b/Modules/Location/routes/web.php index 948ee2d43..b5e9263e3 100644 --- a/Modules/Location/routes/web.php +++ b/Modules/Location/routes/web.php @@ -2,9 +2,17 @@ use Illuminate\Support\Facades\Route; 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( $country->cities() - ->where('is_active', true) ->orderBy('name') ->get(['id', 'name', 'country_id']) ); diff --git a/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php b/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php index c7cced8e1..bac76a9e6 100644 --- a/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php +++ b/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php @@ -6,6 +6,7 @@ use App\Support\QuickListingCategorySuggester; use Filament\Facades\Filament; use Filament\Notifications\Notification; use Filament\Resources\Pages\Page; +use Filament\Support\Enums\Width; use Illuminate\Support\Collection; use Illuminate\Support\Str; 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 $slug = 'quick-create'; protected static bool $shouldRegisterNavigation = false; + protected Width | string | null $maxContentWidth = Width::Full; /** * @var array diff --git a/Modules/Partner/Providers/PartnerPanelProvider.php b/Modules/Partner/Providers/PartnerPanelProvider.php index 4b5413ae0..f31c2b29a 100644 --- a/Modules/Partner/Providers/PartnerPanelProvider.php +++ b/Modules/Partner/Providers/PartnerPanelProvider.php @@ -3,10 +3,8 @@ namespace Modules\Partner\Providers; use A909M\FilamentStateFusion\FilamentStateFusionPlugin; use App\Models\User; -use App\Settings\GeneralSettings; use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin; use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin; -use DutchCodingCompany\FilamentSocialite\Provider; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; @@ -25,8 +23,8 @@ use Illuminate\Support\Str; use Illuminate\View\Middleware\ShareErrorsFromSession; use Jeffgreco13\FilamentBreezy\BreezyCore; use Laravel\Socialite\Contracts\User as SocialiteUserContract; +use Modules\Partner\Support\Filament\SocialiteProviderResolver; use Spatie\Permission\Models\Role; -use Throwable; class PartnerPanelProvider extends PanelProvider { @@ -79,7 +77,7 @@ class PartnerPanelProvider extends PanelProvider private static function socialitePlugin(): FilamentSocialitePlugin { return FilamentSocialitePlugin::make() - ->providers(self::socialiteProviders()) + ->providers(SocialiteProviderResolver::providers()) ->registration(true) ->resolveUserUsing(function (string $provider, SocialiteUserContract $oauthUser): ?User { if (! filled($oauthUser->getEmail())) { @@ -111,60 +109,6 @@ class PartnerPanelProvider extends PanelProvider }); } - /** - * @return array - */ - 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 { $partner = User::query()->where('email', 'b@b.com')->first(); diff --git a/Modules/Partner/Support/Filament/SocialiteProviderResolver.php b/Modules/Partner/Support/Filament/SocialiteProviderResolver.php new file mode 100644 index 000000000..87db3509c --- /dev/null +++ b/Modules/Partner/Support/Filament/SocialiteProviderResolver.php @@ -0,0 +1,62 @@ +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")); + } + } +} diff --git a/Modules/Theme/Providers/ThemeServiceProvider.php b/Modules/Theme/Providers/ThemeServiceProvider.php new file mode 100644 index 000000000..bebbe5ae5 --- /dev/null +++ b/Modules/Theme/Providers/ThemeServiceProvider.php @@ -0,0 +1,18 @@ +mergeConfigFrom(module_path('Theme', 'config/theme.php'), 'theme'); + + $this->app->singleton(ThemeManager::class, function ($app): ThemeManager { + return new ThemeManager($app['config']); + }); + } +} diff --git a/Modules/Theme/Support/ThemeManager.php b/Modules/Theme/Support/ThemeManager.php new file mode 100644 index 000000000..030b5d7a5 --- /dev/null +++ b/Modules/Theme/Support/ThemeManager.php @@ -0,0 +1,50 @@ +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}"; + } +} diff --git a/Modules/Theme/config/theme.php b/Modules/Theme/config/theme.php new file mode 100644 index 000000000..f9d51d910 --- /dev/null +++ b/Modules/Theme/config/theme.php @@ -0,0 +1,9 @@ + env('OC_THEME', 'otoplus'), + 'modules' => [ + 'listing' => env('OC_THEME_LISTING', 'otoplus'), + 'category' => env('OC_THEME_CATEGORY', 'otoplus'), + ], +]; diff --git a/Modules/Theme/module.json b/Modules/Theme/module.json new file mode 100644 index 000000000..13d90e01a --- /dev/null +++ b/Modules/Theme/module.json @@ -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": [] +} diff --git a/app/Http/Controllers/FavoriteController.php b/app/Http/Controllers/FavoriteController.php index 7d02a50c5..c0c37269f 100644 --- a/app/Http/Controllers/FavoriteController.php +++ b/app/Http/Controllers/FavoriteController.php @@ -76,7 +76,7 @@ class FavoriteController extends Controller 'listing:id,title,price,currency,user_id', 'buyer:id,name', 'seller:id,name', - 'lastMessage:id,conversation_id,sender_id,body,created_at', + 'lastMessage', 'lastMessage.sender:id,name', ]) ->withCount([ diff --git a/app/Http/Controllers/PanelController.php b/app/Http/Controllers/PanelController.php index 618dc9175..0f824e25f 100644 --- a/app/Http/Controllers/PanelController.php +++ b/app/Http/Controllers/PanelController.php @@ -80,7 +80,7 @@ class PanelController extends Controller 'listing:id,title,price,currency,user_id', 'buyer:id,name', 'seller:id,name', - 'lastMessage:id,conversation_id,sender_id,body,created_at', + 'lastMessage', ]) ->withCount([ 'messages as unread_count' => fn ($query) => $query diff --git a/app/Livewire/PanelQuickListingForm.php b/app/Livewire/PanelQuickListingForm.php index 0f3484430..fdb545687 100644 --- a/app/Livewire/PanelQuickListingForm.php +++ b/app/Livewire/PanelQuickListingForm.php @@ -3,7 +3,6 @@ namespace App\Livewire; use App\Support\QuickListingCategorySuggester; -use Illuminate\Http\RedirectResponse; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Validation\Rule; @@ -171,10 +170,10 @@ class PanelQuickListingForm extends Component $this->loadListingCustomFields(); } - public function publishListing(): ?RedirectResponse + public function publishListing(): void { if ($this->isPublishing) { - return null; + return; } $this->isPublishing = true; @@ -191,13 +190,13 @@ class PanelQuickListingForm extends Component $this->isPublishing = false; session()->flash('error', 'İlan oluşturulamadı. Lütfen tekrar deneyin.'); - return null; + return; } $this->isPublishing = false; session()->flash('success', 'İlan başarıyla oluşturuldu.'); - return redirect()->route('panel.listings.index'); + $this->redirectRoute('panel.listings.index'); } public function getRootCategoriesProperty(): array diff --git a/app/Models/Conversation.php b/app/Models/Conversation.php index 305754ce1..e8038dcee 100644 --- a/app/Models/Conversation.php +++ b/app/Models/Conversation.php @@ -44,7 +44,15 @@ class Conversation extends Model 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 @@ -55,4 +63,14 @@ class Conversation extends Model ->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; + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index dd4f199c9..0fb14e909 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\View; +use Modules\Category\Models\Category; use Modules\Location\Models\Country; use SocialiteProviders\Manager\SocialiteWasCalled; use Throwable; @@ -193,6 +194,7 @@ class AppServiceProvider extends ServiceProvider }); $headerLocationCountries = []; + $headerNavCategories = []; try { if (Schema::hasTable('countries') && Schema::hasTable('cities')) { @@ -214,8 +216,29 @@ class AppServiceProvider extends ServiceProvider $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('headerLocationCountries', $headerLocationCountries); + View::share('headerNavCategories', $headerNavCategories); } private function normalizeCurrencies(array $currencies): array diff --git a/modules_statuses.json b/modules_statuses.json index 35aa86625..9feee4626 100644 --- a/modules_statuses.json +++ b/modules_statuses.json @@ -4,5 +4,6 @@ "Location": true, "Profile": true, "Admin": true, - "Partner": false + "Partner": false, + "Theme": true } diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index eb0536286..000000000 --- a/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: diff --git a/resources/views/filament/partner/listings/quick-create.blade.php b/resources/views/filament/partner/listings/quick-create.blade.php index 00ace84d0..70028226e 100644 --- a/resources/views/filament/partner/listings/quick-create.blade.php +++ b/resources/views/filament/partner/listings/quick-create.blade.php @@ -1,1287 +1 @@ -
- - -
-
-

{{ $this->currentStepTitle }}

-
- -
{{ $currentStep }}/5
-
-
- -
- @if ($currentStep === 1) -
- - - - -

- İpucu: En az 1 fotoğraf, en çok {{ (int) config('quick-listing.max_photo_count', 20) }} fotoğraf yükleyebilirsin.
- Desteklenen formatlar: .jpg, .jpeg ve .png -

- - @error('photos') -
{{ $message }}
- @enderror - - @error('photos.*') -
{{ $message }}
- @enderror - - @if (count($photos) > 0) -

Seçtiğin Fotoğraflar

-
Fotoğrafları sıralamak için tut ve sürükle
- -
- @for ($index = 0; $index < (int) config('quick-listing.max_photo_count', 20); $index++) -
- @if (isset($photos[$index])) - Yüklenen fotoğraf {{ $index + 1 }} - - @if ($index === 0) -
KAPAK
- @endif - @else - - @endif -
- @endfor -
- @else -
- -

Ürün fotoğraflarını yükle

-

- Hızlı ilan vermek için en az 1 fotoğraf yükleyin.
- Laravel AI sizin için otomatik kategori önerileri sunar. -

-
- @endif -
- - - @endif - - @if ($currentStep === 2) - @if ($isDetecting) -
- - Fotoğraf analiz ediliyor, kategori önerisi hazırlanıyor... -
- @elseif ($detectedCategoryId) -
- - - AI kategori önerdi: {{ $this->selectedCategoryName }} - @if ($detectedConfidence) - (Güven: {{ number_format($detectedConfidence * 100, 0) }}%) - @endif - @if ($detectedReason) - {{ $detectedReason }} - @endif - -
- @else -
- - - AI ile kategori tespit edilemedi, lütfen kategori seçimi yapın. - @if ($detectedError) - {{ $detectedError }} - @endif - -
- @endif - - @if ($detectedAlternatives !== []) -
- @foreach ($detectedAlternatives as $alternativeId) - @php - $alternativeCategory = collect($categories)->firstWhere('id', $alternativeId); - @endphp - @if ($alternativeCategory) - - @endif - @endforeach -
- @endif - - @if (is_null($activeParentCategoryId)) -
- - Ne Satıyorsun? - -
- -
- @foreach ($this->rootCategories as $category) - - @endforeach -
- @else -
- - {{ $this->currentParentName }} - -
- - - -
- @forelse ($this->currentCategories as $category) -
- - - @if ($category['has_children'] && $category['id'] !== $activeParentCategoryId) - - @else - - @endif - - - @if ($selectedCategoryId === $category['id']) - - @endif - -
- @empty -
- Aramaya uygun kategori bulunamadı. -
- @endforelse -
- @endif - - @if ($errors->has('selectedCategoryId')) -
{{ $errors->first('selectedCategoryId') }}
- @endif - - @if ($this->selectedCategoryName) -
Seçilen kategori: {{ $this->selectedCategoryName }}
- @endif - - - @endif - - @if ($currentStep === 3) -
-
- @foreach (array_slice($photos, 0, 7) as $index => $photo) -
- Seçilen fotoğraf {{ $index + 1 }} - - @if ($index === 0) -
KAPAK
- @endif -
- @endforeach -
- -
-
-

Seçilen Kategori

-

{{ $this->selectedCategoryPath ?: '-' }}

-
- -
- -
-
- - -

Ürünün temel özelliklerinden bahset (ör. marka, model, yaş, tip)

-
{{ $this->titleCharacters }}/70
- @error('listingTitle')
{{ $message }}
@enderror -
- -
- -
- - {{ \Modules\Listing\Support\ListingPanelHelper::defaultCurrency() }} -
-

Lütfen unutma; doğru fiyat daha hızlı satmanıza yardımcı olacaktır

- @error('price')
{{ $message }}
@enderror -
- -
- - -

Durum, özellik ve satma nedeni gibi bilgileri ekle

-
{{ $this->descriptionCharacters }}/1450
- @error('description')
{{ $message }}
@enderror -
- -
- -
-
- - @error('selectedCountryId')
{{ $message }}
@enderror -
-
- - @error('selectedCityId')
{{ $message }}
@enderror -
-
-
-
-
- - - @endif - - @if ($currentStep === 4) -
-
-
-

Seçilen Kategori

-

{{ $this->selectedCategoryPath ?: '-' }}

-
- -
- - @if ($listingCustomFields === []) -
- Bu kategori için ek ilan özelliği tanımlı değil. Devam ederek önizleme adımına geçebilirsin. -
- @else -
- @foreach ($listingCustomFields as $field) -
- - - @if ($field['type'] === 'text') - - @elseif ($field['type'] === 'textarea') - - @elseif ($field['type'] === 'number') - - @elseif ($field['type'] === 'select') - - @elseif ($field['type'] === 'boolean') - - @elseif ($field['type'] === 'date') - - @endif - - @if ($field['help_text']) -

{{ $field['help_text'] }}

- @endif - - @error('customFieldValues.'.$field['name']) -
{{ $message }}
- @enderror -
- @endforeach -
- @endif -
- - - @endif - - @if ($currentStep === 5) -
-
Anasayfa › {{ $this->selectedCategoryPath }}
- -
-
- - - @php - $displayPrice = is_numeric($price) ? number_format((float) $price, 0, ',', '.') : $price; - @endphp - -
-
{{ $displayPrice }} {{ \Modules\Listing\Support\ListingPanelHelper::defaultCurrency() }}
-
- - {{ $this->selectedCityName ?: '-' }}, {{ $this->selectedCountryName ?: '-' }} - {{ now()->format('d.m.Y') }} -
-
{{ $listingTitle }}
-

{{ $description }}

-
- -
-
İlan Özellikleri
- @if ($this->previewCustomFields !== []) - @foreach ($this->previewCustomFields as $field) -
-
{{ $field['label'] }}
-
{{ $field['value'] }}
-
- @endforeach - @else -
-
Ek özellik
-
Bu kategori için seçilmedi
-
- @endif -
-
- -
-
-
- {{ $this->currentUserInitial }} -
-
{{ $this->currentUserName }}
-
{{ auth()->user()?->email }}
-
-
- -
-
Harita
-
Satıcı Profili
-
-
- -
- - -
-
-
-
- @endif -
-
-
+@include('partials.quick-create.form') diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index db8eba399..291ec0b5f 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -27,13 +27,11 @@ 'ru' => 'Русский', 'ja' => '日本語', ]; - $isHomePage = request()->routeIs('home'); - $isSimplePage = trim($__env->yieldContent('simple_page')) === '1'; - $homeHeaderCategories = isset($categories) ? collect($categories)->take(8) : collect(); + $headerCategories = collect($headerNavCategories ?? [])->values(); $locationCountries = collect($headerLocationCountries ?? [])->values(); $defaultCountryIso2 = strtoupper((string) config('app.default_country_iso2', 'TR')); $citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities') - ? route('locations.cities', ['country' => '__COUNTRY__']) + ? route('locations.cities', ['country' => '__COUNTRY__'], false) : ''; @endphp @@ -258,7 +256,6 @@ - @if(!$isSimplePage && $isHomePage && $homeHeaderCategories->isNotEmpty()) - @elseif(! $isSimplePage && ! $isHomePage) - - @endif @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 citySelect = root.querySelector('[data-location-city]'); const countrySelect = root.querySelector('[data-location-country]'); @@ -432,20 +446,32 @@ citySelect.innerHTML = ''; try { - const response = await fetch(template.replace('__COUNTRY__', encodeURIComponent(String(countryId))), { - headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Accept': 'application/json', - }, - }); + const primaryUrl = template.replace('__COUNTRY__', encodeURIComponent(String(countryId))); + let cityOptions; - if (!response.ok) { - throw new Error('city_fetch_failed'); + try { + 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 = ''; cityOptions.forEach((city) => { diff --git a/resources/views/panel/create.blade.php b/resources/views/panel/create.blade.php index 14024571c..8a58cbc09 100644 --- a/resources/views/panel/create.blade.php +++ b/resources/views/panel/create.blade.php @@ -5,9 +5,5 @@ @section('simple_page', '1') @section('content') -
-
- -
-
+ @endsection diff --git a/resources/views/panel/quick-create.blade.php b/resources/views/panel/quick-create.blade.php index 00ace84d0..70028226e 100644 --- a/resources/views/panel/quick-create.blade.php +++ b/resources/views/panel/quick-create.blade.php @@ -1,1287 +1 @@ -
- - -
-
-

{{ $this->currentStepTitle }}

-
- -
{{ $currentStep }}/5
-
-
- -
- @if ($currentStep === 1) -
- - - - -

- İpucu: En az 1 fotoğraf, en çok {{ (int) config('quick-listing.max_photo_count', 20) }} fotoğraf yükleyebilirsin.
- Desteklenen formatlar: .jpg, .jpeg ve .png -

- - @error('photos') -
{{ $message }}
- @enderror - - @error('photos.*') -
{{ $message }}
- @enderror - - @if (count($photos) > 0) -

Seçtiğin Fotoğraflar

-
Fotoğrafları sıralamak için tut ve sürükle
- -
- @for ($index = 0; $index < (int) config('quick-listing.max_photo_count', 20); $index++) -
- @if (isset($photos[$index])) - Yüklenen fotoğraf {{ $index + 1 }} - - @if ($index === 0) -
KAPAK
- @endif - @else - - @endif -
- @endfor -
- @else -
- -

Ürün fotoğraflarını yükle

-

- Hızlı ilan vermek için en az 1 fotoğraf yükleyin.
- Laravel AI sizin için otomatik kategori önerileri sunar. -

-
- @endif -
- - - @endif - - @if ($currentStep === 2) - @if ($isDetecting) -
- - Fotoğraf analiz ediliyor, kategori önerisi hazırlanıyor... -
- @elseif ($detectedCategoryId) -
- - - AI kategori önerdi: {{ $this->selectedCategoryName }} - @if ($detectedConfidence) - (Güven: {{ number_format($detectedConfidence * 100, 0) }}%) - @endif - @if ($detectedReason) - {{ $detectedReason }} - @endif - -
- @else -
- - - AI ile kategori tespit edilemedi, lütfen kategori seçimi yapın. - @if ($detectedError) - {{ $detectedError }} - @endif - -
- @endif - - @if ($detectedAlternatives !== []) -
- @foreach ($detectedAlternatives as $alternativeId) - @php - $alternativeCategory = collect($categories)->firstWhere('id', $alternativeId); - @endphp - @if ($alternativeCategory) - - @endif - @endforeach -
- @endif - - @if (is_null($activeParentCategoryId)) -
- - Ne Satıyorsun? - -
- -
- @foreach ($this->rootCategories as $category) - - @endforeach -
- @else -
- - {{ $this->currentParentName }} - -
- - - -
- @forelse ($this->currentCategories as $category) -
- - - @if ($category['has_children'] && $category['id'] !== $activeParentCategoryId) - - @else - - @endif - - - @if ($selectedCategoryId === $category['id']) - - @endif - -
- @empty -
- Aramaya uygun kategori bulunamadı. -
- @endforelse -
- @endif - - @if ($errors->has('selectedCategoryId')) -
{{ $errors->first('selectedCategoryId') }}
- @endif - - @if ($this->selectedCategoryName) -
Seçilen kategori: {{ $this->selectedCategoryName }}
- @endif - - - @endif - - @if ($currentStep === 3) -
-
- @foreach (array_slice($photos, 0, 7) as $index => $photo) -
- Seçilen fotoğraf {{ $index + 1 }} - - @if ($index === 0) -
KAPAK
- @endif -
- @endforeach -
- -
-
-

Seçilen Kategori

-

{{ $this->selectedCategoryPath ?: '-' }}

-
- -
- -
-
- - -

Ürünün temel özelliklerinden bahset (ör. marka, model, yaş, tip)

-
{{ $this->titleCharacters }}/70
- @error('listingTitle')
{{ $message }}
@enderror -
- -
- -
- - {{ \Modules\Listing\Support\ListingPanelHelper::defaultCurrency() }} -
-

Lütfen unutma; doğru fiyat daha hızlı satmanıza yardımcı olacaktır

- @error('price')
{{ $message }}
@enderror -
- -
- - -

Durum, özellik ve satma nedeni gibi bilgileri ekle

-
{{ $this->descriptionCharacters }}/1450
- @error('description')
{{ $message }}
@enderror -
- -
- -
-
- - @error('selectedCountryId')
{{ $message }}
@enderror -
-
- - @error('selectedCityId')
{{ $message }}
@enderror -
-
-
-
-
- - - @endif - - @if ($currentStep === 4) -
-
-
-

Seçilen Kategori

-

{{ $this->selectedCategoryPath ?: '-' }}

-
- -
- - @if ($listingCustomFields === []) -
- Bu kategori için ek ilan özelliği tanımlı değil. Devam ederek önizleme adımına geçebilirsin. -
- @else -
- @foreach ($listingCustomFields as $field) -
- - - @if ($field['type'] === 'text') - - @elseif ($field['type'] === 'textarea') - - @elseif ($field['type'] === 'number') - - @elseif ($field['type'] === 'select') - - @elseif ($field['type'] === 'boolean') - - @elseif ($field['type'] === 'date') - - @endif - - @if ($field['help_text']) -

{{ $field['help_text'] }}

- @endif - - @error('customFieldValues.'.$field['name']) -
{{ $message }}
- @enderror -
- @endforeach -
- @endif -
- - - @endif - - @if ($currentStep === 5) -
-
Anasayfa › {{ $this->selectedCategoryPath }}
- -
-
- - - @php - $displayPrice = is_numeric($price) ? number_format((float) $price, 0, ',', '.') : $price; - @endphp - -
-
{{ $displayPrice }} {{ \Modules\Listing\Support\ListingPanelHelper::defaultCurrency() }}
-
- - {{ $this->selectedCityName ?: '-' }}, {{ $this->selectedCountryName ?: '-' }} - {{ now()->format('d.m.Y') }} -
-
{{ $listingTitle }}
-

{{ $description }}

-
- -
-
İlan Özellikleri
- @if ($this->previewCustomFields !== []) - @foreach ($this->previewCustomFields as $field) -
-
{{ $field['label'] }}
-
{{ $field['value'] }}
-
- @endforeach - @else -
-
Ek özellik
-
Bu kategori için seçilmedi
-
- @endif -
-
- -
-
-
- {{ $this->currentUserInitial }} -
-
{{ $this->currentUserName }}
-
{{ auth()->user()?->email }}
-
-
- -
-
Harita
-
Satıcı Profili
-
-
- -
- - -
-
-
-
- @endif -
-
-
+@include('partials.quick-create.form') diff --git a/resources/views/partials/quick-create/form.blade.php b/resources/views/partials/quick-create/form.blade.php new file mode 100644 index 000000000..3044d989e --- /dev/null +++ b/resources/views/partials/quick-create/form.blade.php @@ -0,0 +1,1333 @@ +
+ + +
+
+

{{ $this->currentStepTitle }}

+
+ +
{{ $currentStep }}/5
+
+
+ +
+ @if ($currentStep === 1) +
+ + + + +

+ İpucu: En az 1 fotoğraf, en çok {{ (int) config('quick-listing.max_photo_count', 20) }} fotoğraf yükleyebilirsin.
+ Desteklenen formatlar: .jpg, .jpeg ve .png +

+ + @error('photos') +
{{ $message }}
+ @enderror + + @error('photos.*') +
{{ $message }}
+ @enderror + + @if (count($photos) > 0) +

Seçtiğin Fotoğraflar

+
Fotoğrafları sıralamak için tut ve sürükle
+ +
+ @for ($index = 0; $index < (int) config('quick-listing.max_photo_count', 20); $index++) +
+ @if (isset($photos[$index])) + Yüklenen fotoğraf {{ $index + 1 }} + + @if ($index === 0) +
KAPAK
+ @endif + @else + + @endif +
+ @endfor +
+ @else +
+ +

Ürün fotoğraflarını yükle

+

+ Hızlı ilan vermek için en az 1 fotoğraf yükleyin.
+ Laravel AI sizin için otomatik kategori önerileri sunar. +

+
+ @endif +
+ + + @endif + + @if ($currentStep === 2) + @if ($isDetecting) +
+ + Fotoğraf analiz ediliyor, kategori önerisi hazırlanıyor... +
+ @elseif ($detectedCategoryId) +
+ + + AI kategori önerdi: {{ $this->selectedCategoryName }} + @if ($detectedConfidence) + (Güven: {{ number_format($detectedConfidence * 100, 0) }}%) + @endif + @if ($detectedReason) + {{ $detectedReason }} + @endif + +
+ @else +
+ + + AI ile kategori tespit edilemedi, lütfen kategori seçimi yapın. + @if ($detectedError) + {{ $detectedError }} + @endif + +
+ @endif + + @if ($detectedAlternatives !== []) +
+ @foreach ($detectedAlternatives as $alternativeId) + @php + $alternativeCategory = collect($categories)->firstWhere('id', $alternativeId); + @endphp + @if ($alternativeCategory) + + @endif + @endforeach +
+ @endif + + @if (is_null($activeParentCategoryId)) +
+ + Ne Satıyorsun? + +
+ +
+ @foreach ($this->rootCategories as $category) + + @endforeach +
+ @else +
+ + {{ $this->currentParentName }} + +
+ + + +
+ @forelse ($this->currentCategories as $category) +
+ + + @if ($category['has_children'] && $category['id'] !== $activeParentCategoryId) + + @else + + @endif + + + @if ($selectedCategoryId === $category['id']) + + @endif + +
+ @empty +
+ Aramaya uygun kategori bulunamadı. +
+ @endforelse +
+ @endif + + @if ($errors->has('selectedCategoryId')) +
{{ $errors->first('selectedCategoryId') }}
+ @endif + + @if ($this->selectedCategoryName) +
Seçilen kategori: {{ $this->selectedCategoryName }}
+ @endif + + + @endif + + @if ($currentStep === 3) +
+
+ @foreach (array_slice($photos, 0, 7) as $index => $photo) +
+ Seçilen fotoğraf {{ $index + 1 }} + + @if ($index === 0) +
KAPAK
+ @endif +
+ @endforeach +
+ +
+
+

Seçilen Kategori

+

{{ $this->selectedCategoryPath ?: '-' }}

+
+ +
+ +
+
+ + +

Ürünün temel özelliklerinden bahset (ör. marka, model, yaş, tip)

+
{{ $this->titleCharacters }}/70
+ @error('listingTitle')
{{ $message }}
@enderror +
+ +
+ +
+ + {{ \Modules\Listing\Support\ListingPanelHelper::defaultCurrency() }} +
+

Lütfen unutma; doğru fiyat daha hızlı satmanıza yardımcı olacaktır

+ @error('price')
{{ $message }}
@enderror +
+ +
+ + +

Durum, özellik ve satma nedeni gibi bilgileri ekle

+
{{ $this->descriptionCharacters }}/1450
+ @error('description')
{{ $message }}
@enderror +
+ +
+ +
+
+ + @error('selectedCountryId')
{{ $message }}
@enderror +
+
+ + @error('selectedCityId')
{{ $message }}
@enderror +
+
+
+
+
+ + + @endif + + @if ($currentStep === 4) +
+
+
+

Seçilen Kategori

+

{{ $this->selectedCategoryPath ?: '-' }}

+
+ +
+ + @if ($listingCustomFields === []) +
+ Bu kategori için ek ilan özelliği tanımlı değil. Devam ederek önizleme adımına geçebilirsin. +
+ @else +
+ @foreach ($listingCustomFields as $field) +
+ + + @if ($field['type'] === 'text') + + @elseif ($field['type'] === 'textarea') + + @elseif ($field['type'] === 'number') + + @elseif ($field['type'] === 'select') + + @elseif ($field['type'] === 'boolean') + + @elseif ($field['type'] === 'date') + + @endif + + @if ($field['help_text']) +

{{ $field['help_text'] }}

+ @endif + + @error('customFieldValues.'.$field['name']) +
{{ $message }}
+ @enderror +
+ @endforeach +
+ @endif +
+ + + @endif + + @if ($currentStep === 5) +
+
Anasayfa › {{ $this->selectedCategoryPath }}
+ +
+
+ + + @php + $displayPrice = is_numeric($price) ? number_format((float) $price, 0, ',', '.') : $price; + @endphp + +
+
{{ $displayPrice }} {{ \Modules\Listing\Support\ListingPanelHelper::defaultCurrency() }}
+
+ + {{ $this->selectedCityName ?: '-' }}, {{ $this->selectedCountryName ?: '-' }} + {{ now()->format('d.m.Y') }} +
+
{{ $listingTitle }}
+

{{ $description }}

+
+ +
+
İlan Özellikleri
+ @if ($this->previewCustomFields !== []) + @foreach ($this->previewCustomFields as $field) +
+
{{ $field['label'] }}
+
{{ $field['value'] }}
+
+ @endforeach + @else +
+
Ek özellik
+
Bu kategori için seçilmedi
+
+ @endif +
+
+ +
+
+
+ {{ $this->currentUserInitial }} +
+
{{ $this->currentUserName }}
+
{{ auth()->user()?->email }}
+
+
+ +
+
Harita
+
Satıcı Profili
+
+
+ +
+ + +
+
+
+
+ @endif +
+
+