diff --git a/.env.example b/.env.example
index 9ce159055..5f8ec325a 100644
--- a/.env.example
+++ b/.env.example
@@ -51,3 +51,8 @@ MAIL_FROM_ADDRESS="hello@openclassify.com"
MAIL_FROM_NAME="${APP_NAME}"
VITE_APP_NAME="${APP_NAME}"
+
+OPENAI_API_KEY=
+GEMINI_API_KEY=
+QUICK_LISTING_AI_PROVIDER=openai
+QUICK_LISTING_AI_MODEL=gpt-5.2
diff --git a/Modules/Admin/Filament/Resources/CityResource.php b/Modules/Admin/Filament/Resources/CityResource.php
new file mode 100644
index 000000000..877f3f15d
--- /dev/null
+++ b/Modules/Admin/Filament/Resources/CityResource.php
@@ -0,0 +1,74 @@
+schema([
+ TextInput::make('name')->required()->maxLength(120),
+ Select::make('country_id')->relationship('country', 'name')->label('Country')->searchable()->preload()->required(),
+ Toggle::make('is_active')->default(true),
+ ]);
+ }
+
+ public static function table(Table $table): Table
+ {
+ return $table->columns([
+ TextColumn::make('id')->sortable(),
+ TextColumn::make('name')->searchable()->sortable(),
+ TextColumn::make('country.name')->label('Country')->searchable()->sortable(),
+ TextColumn::make('districts_count')->counts('districts')->label('Districts')->sortable(),
+ IconColumn::make('is_active')->boolean(),
+ TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true),
+ ])->filters([
+ SelectFilter::make('country_id')
+ ->label('Country')
+ ->relationship('country', 'name')
+ ->searchable()
+ ->preload(),
+ TernaryFilter::make('is_active')->label('Active'),
+ ])->actions([
+ EditAction::make(),
+ Action::make('activities')
+ ->icon('heroicon-o-clock')
+ ->url(fn (City $record): string => static::getUrl('activities', ['record' => $record])),
+ DeleteAction::make(),
+ ]);
+ }
+
+ public static function getPages(): array
+ {
+ return [
+ 'index' => Pages\ListCities::route('/'),
+ 'create' => Pages\CreateCity::route('/create'),
+ 'activities' => Pages\ListCityActivities::route('/{record}/activities'),
+ 'edit' => Pages\EditCity::route('/{record}/edit'),
+ ];
+ }
+}
diff --git a/Modules/Admin/Filament/Resources/CityResource/Pages/CreateCity.php b/Modules/Admin/Filament/Resources/CityResource/Pages/CreateCity.php
new file mode 100644
index 000000000..6d0069b90
--- /dev/null
+++ b/Modules/Admin/Filament/Resources/CityResource/Pages/CreateCity.php
@@ -0,0 +1,10 @@
+schema([
+ TextInput::make('name')->required()->maxLength(120),
+ Select::make('city_id')->relationship('city', 'name')->label('City')->searchable()->preload()->required(),
+ Toggle::make('is_active')->default(true),
+ ]);
+ }
+
+ public static function table(Table $table): Table
+ {
+ return $table->columns([
+ TextColumn::make('id')->sortable(),
+ TextColumn::make('name')->searchable()->sortable(),
+ TextColumn::make('city.name')->label('City')->searchable()->sortable(),
+ TextColumn::make('city.country.name')->label('Country'),
+ IconColumn::make('is_active')->boolean(),
+ TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true),
+ ])->filters([
+ SelectFilter::make('country_id')
+ ->label('Country')
+ ->options(fn (): array => Country::query()->orderBy('name')->pluck('name', 'id')->all())
+ ->query(fn (Builder $query, array $data): Builder => $query->when($data['value'] ?? null, fn (Builder $query, string $countryId): Builder => $query->whereHas('city', fn (Builder $cityQuery): Builder => $cityQuery->where('country_id', $countryId)))),
+ SelectFilter::make('city_id')
+ ->label('City')
+ ->relationship('city', 'name')
+ ->searchable()
+ ->preload(),
+ TernaryFilter::make('is_active')->label('Active'),
+ ])->actions([
+ EditAction::make(),
+ Action::make('activities')
+ ->icon('heroicon-o-clock')
+ ->url(fn (District $record): string => static::getUrl('activities', ['record' => $record])),
+ DeleteAction::make(),
+ ]);
+ }
+
+ public static function getPages(): array
+ {
+ return [
+ 'index' => Pages\ListDistricts::route('/'),
+ 'create' => Pages\CreateDistrict::route('/create'),
+ 'activities' => Pages\ListDistrictActivities::route('/{record}/activities'),
+ 'edit' => Pages\EditDistrict::route('/{record}/edit'),
+ ];
+ }
+}
diff --git a/Modules/Admin/Filament/Resources/DistrictResource/Pages/CreateDistrict.php b/Modules/Admin/Filament/Resources/DistrictResource/Pages/CreateDistrict.php
new file mode 100644
index 000000000..099af6f85
--- /dev/null
+++ b/Modules/Admin/Filament/Resources/DistrictResource/Pages/CreateDistrict.php
@@ -0,0 +1,10 @@
+count();
+ $activeListings = Listing::query()->where('status', 'active')->count();
+ $pendingListings = Listing::query()->where('status', 'pending')->count();
+ $featuredListings = Listing::query()->where('is_featured', true)->count();
+ $createdToday = Listing::query()->where('created_at', '>=', now()->startOfDay())->count();
+
+ $featuredRatio = $totalListings > 0
+ ? number_format(($featuredListings / $totalListings) * 100, 1).'% of all listings'
+ : '0.0% of all listings';
+
+ return [
+ Stat::make('Total Listings', number_format($totalListings))
+ ->description('All listings in the system')
+ ->icon('heroicon-o-clipboard-document-list')
+ ->color('primary'),
+ Stat::make('Active Listings', number_format($activeListings))
+ ->description(number_format($pendingListings).' pending review')
+ ->descriptionIcon('heroicon-o-clock')
+ ->icon('heroicon-o-check-circle')
+ ->color('success'),
+ Stat::make('Created Today', number_format($createdToday))
+ ->description('New listings added today')
+ ->icon('heroicon-o-calendar-days')
+ ->color('info'),
+ Stat::make('Featured Listings', number_format($featuredListings))
+ ->description($featuredRatio)
+ ->icon('heroicon-o-star')
+ ->color('warning'),
+ ];
+ }
+}
diff --git a/Modules/Admin/Filament/Widgets/ListingsTrendChart.php b/Modules/Admin/Filament/Widgets/ListingsTrendChart.php
new file mode 100644
index 000000000..416f32710
--- /dev/null
+++ b/Modules/Admin/Filament/Widgets/ListingsTrendChart.php
@@ -0,0 +1,67 @@
+ 'Last 7 days',
+ '30' => 'Last 30 days',
+ '90' => 'Last 90 days',
+ ];
+ }
+
+ protected function getData(): array
+ {
+ $days = (int) ($this->filter ?? '30');
+ $startDate = now()->startOfDay()->subDays($days - 1);
+
+ $countsByDate = Listing::query()
+ ->selectRaw('DATE(created_at) as day, COUNT(*) as total')
+ ->where('created_at', '>=', $startDate)
+ ->groupBy('day')
+ ->orderBy('day')
+ ->pluck('total', 'day')
+ ->all();
+
+ $labels = [];
+ $data = [];
+
+ for ($index = 0; $index < $days; $index++) {
+ $date = $startDate->copy()->addDays($index);
+ $dateKey = $date->toDateString();
+
+ $labels[] = $date->format('M j');
+ $data[] = (int) ($countsByDate[$dateKey] ?? 0);
+ }
+
+ return [
+ 'datasets' => [
+ [
+ 'label' => 'Listings',
+ 'data' => $data,
+ 'fill' => true,
+ 'borderColor' => '#2563eb',
+ 'backgroundColor' => 'rgba(37, 99, 235, 0.12)',
+ 'tension' => 0.35,
+ ],
+ ],
+ 'labels' => $labels,
+ ];
+ }
+
+ protected function getType(): string
+ {
+ return 'line';
+ }
+}
diff --git a/Modules/Listing/Http/Controllers/ListingController.php b/Modules/Listing/Http/Controllers/ListingController.php
index 5ff7611ed..0870c6404 100644
--- a/Modules/Listing/Http/Controllers/ListingController.php
+++ b/Modules/Listing/Http/Controllers/ListingController.php
@@ -2,6 +2,8 @@
namespace Modules\Listing\Http\Controllers;
use App\Http\Controllers\Controller;
+use App\Models\FavoriteSearch;
+use Modules\Category\Models\Category;
use Modules\Listing\Models\Listing;
class ListingController extends Controller
@@ -9,9 +11,12 @@ class ListingController extends Controller
public function index()
{
$search = trim((string) request('search', ''));
+ $categoryId = request()->integer('category');
+ $categoryId = $categoryId > 0 ? $categoryId : null;
$listings = Listing::query()
->publicFeed()
+ ->with('category:id,name')
->when($search !== '', function ($query) use ($search): void {
$query->where(function ($searchQuery) use ($search): void {
$searchQuery
@@ -21,15 +26,70 @@ class ListingController extends Controller
->orWhere('country', 'like', "%{$search}%");
});
})
+ ->when($categoryId, fn ($query) => $query->where('category_id', $categoryId))
->paginate(12)
->withQueryString();
- return view('listing::index', compact('listings', 'search'));
+ $categories = Category::query()
+ ->where('is_active', true)
+ ->orderBy('name')
+ ->get(['id', 'name']);
+
+ $favoriteListingIds = [];
+ $isCurrentSearchSaved = false;
+
+ if (auth()->check()) {
+ $favoriteListingIds = auth()->user()
+ ->favoriteListings()
+ ->pluck('listings.id')
+ ->all();
+
+ $filters = FavoriteSearch::normalizeFilters([
+ 'search' => $search,
+ 'category' => $categoryId,
+ ]);
+
+ if ($filters !== []) {
+ $signature = FavoriteSearch::signatureFor($filters);
+ $isCurrentSearchSaved = auth()->user()
+ ->favoriteSearches()
+ ->where('signature', $signature)
+ ->exists();
+ }
+ }
+
+ return view('listing::index', compact(
+ 'listings',
+ 'search',
+ 'categoryId',
+ 'categories',
+ 'favoriteListingIds',
+ 'isCurrentSearchSaved',
+ ));
}
public function show(Listing $listing)
{
- return view('listing::show', compact('listing'));
+ $listing->loadMissing('user:id,name,email');
+
+ $isListingFavorited = false;
+ $isSellerFavorited = false;
+
+ if (auth()->check()) {
+ $isListingFavorited = auth()->user()
+ ->favoriteListings()
+ ->whereKey($listing->getKey())
+ ->exists();
+
+ if ($listing->user_id) {
+ $isSellerFavorited = auth()->user()
+ ->favoriteSellers()
+ ->whereKey($listing->user_id)
+ ->exists();
+ }
+ }
+
+ return view('listing::show', compact('listing', 'isListingFavorited', 'isSellerFavorited'));
}
public function create()
diff --git a/Modules/Listing/Models/Listing.php b/Modules/Listing/Models/Listing.php
index 0139634b2..390393d78 100644
--- a/Modules/Listing/Models/Listing.php
+++ b/Modules/Listing/Models/Listing.php
@@ -55,6 +55,12 @@ class Listing extends Model implements HasMedia
return $this->belongsTo(\App\Models\User::class);
}
+ public function favoritedByUsers()
+ {
+ return $this->belongsToMany(\App\Models\User::class, 'favorite_listings')
+ ->withTimestamps();
+ }
+
public function scopePublicFeed(Builder $query): Builder
{
return $query
diff --git a/Modules/Listing/resources/views/index.blade.php b/Modules/Listing/resources/views/index.blade.php
index 3b24a924f..8a502ec2b 100644
--- a/Modules/Listing/resources/views/index.blade.php
+++ b/Modules/Listing/resources/views/index.blade.php
@@ -1,12 +1,78 @@
@extends('app::layouts.app')
@section('content')
-
-
{{ __('messages.listings') }}
+
+
+
{{ __('messages.listings') }}
+
+
+
+
+ @auth
+ @php
+ $canSaveSearch = $search !== '' || !is_null($categoryId);
+ @endphp
+
+
+ Bu aramayı favorilere ekleyerek daha sonra hızlıca açabilirsin.
+
+
+
+ Favori Aramalar
+
+
+ @endauth
+
@foreach($listings as $listing)
-
-
+ @php
+ $listingImage = $listing->getFirstMediaUrl('listing-images');
+ $isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
+ @endphp
+
+
+ @if($listingImage)
+
+ @else
+ @endif
+
+
+ @auth
+
+ @else
+
+ ♥
+
+ @endauth
+
@if($listing->is_featured)
@@ -16,10 +82,15 @@
@if($listing->price) {{ number_format($listing->price, 0) }} {{ $listing->currency }} @else Free @endif
+
{{ $listing->category?->name ?: 'Kategori yok' }}
{{ $listing->city }}, {{ $listing->country }}
View
+ @empty
+
+ Bu filtreye uygun ilan bulunamadı.
+
@endforeach
{{ $listings->links() }}
diff --git a/Modules/Listing/resources/views/show.blade.php b/Modules/Listing/resources/views/show.blade.php
index 56d7e3eb0..4d95cda9f 100644
--- a/Modules/Listing/resources/views/show.blade.php
+++ b/Modules/Listing/resources/views/show.blade.php
@@ -37,6 +37,28 @@
@endif
+
+ @auth
+
+ @if($listing->user && (int) $listing->user->id !== (int) auth()->id())
+
+ @endif
+ @else
+
+ Giriş yap ve favorile
+
+ @endauth
+
{{ $location !== '' ? $location : 'Location not specified' }}
Posted {{ $listing->created_at?->diffForHumans() ?? 'recently' }}
@@ -45,6 +67,9 @@
Contact Seller
+ @if($listing->user)
+
Name: {{ $listing->user->name }}
+ @endif
@if($listing->contact_phone)
Phone: {{ $listing->contact_phone }}
@endif
diff --git a/Modules/Partner/Filament/Resources/ListingResource.php b/Modules/Partner/Filament/Resources/ListingResource.php
index 79045e570..fdbbaca8e 100644
--- a/Modules/Partner/Filament/Resources/ListingResource.php
+++ b/Modules/Partner/Filament/Resources/ListingResource.php
@@ -11,21 +11,27 @@ use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Facades\Filament;
+use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Resources\Resource;
+use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;
use Filament\Tables\Columns\TextColumn;
+use Filament\Tables\Filters\Filter;
+use Filament\Tables\Filters\SelectFilter;
+use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Modules\Category\Models\Category;
use Modules\Listing\Models\Listing;
use Modules\Listing\Support\ListingPanelHelper;
+use Modules\Location\Models\City;
+use Modules\Location\Models\Country;
use Modules\Partner\Filament\Resources\ListingResource\Pages;
-use Tapp\FilamentCountryCodeField\Forms\Components\CountryCodeSelect;
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
class ListingResource extends Resource
@@ -36,8 +42,33 @@ class ListingResource extends Resource
public static function form(Schema $schema): Schema
{
return $schema->schema([
- TextInput::make('title')->required()->maxLength(255)->live(onBlur: true)->afterStateUpdated(fn ($state, $set) => $set('slug', \Illuminate\Support\Str::slug($state) . '-' . \Illuminate\Support\Str::random(4))),
- TextInput::make('slug')->required()->maxLength(255)->unique(ignoreRecord: true),
+ TextInput::make('title')
+ ->required()
+ ->maxLength(255)
+ ->live(onBlur: true)
+ ->afterStateUpdated(function ($state, $set, ?Listing $record): void {
+ $baseSlug = \Illuminate\Support\Str::slug((string) $state);
+ $baseSlug = $baseSlug !== '' ? $baseSlug : 'listing';
+
+ $slug = $baseSlug;
+ $counter = 1;
+
+ while (Listing::query()
+ ->where('slug', $slug)
+ ->when($record, fn (Builder $query): Builder => $query->whereKeyNot($record->getKey()))
+ ->exists()) {
+ $slug = "{$baseSlug}-{$counter}";
+ $counter++;
+ }
+
+ $set('slug', $slug);
+ }),
+ TextInput::make('slug')
+ ->required()
+ ->maxLength(255)
+ ->unique(ignoreRecord: true)
+ ->readOnly()
+ ->helperText('Slug is generated automatically from title.'),
Textarea::make('description')->rows(4),
TextInput::make('price')
->numeric()
@@ -46,16 +77,53 @@ class ListingResource extends Resource
->options(fn () => ListingPanelHelper::currencyOptions())
->default(fn () => ListingPanelHelper::defaultCurrency())
->required(),
- Select::make('category_id')->label('Category')->options(fn () => Category::where('is_active', true)->pluck('name', 'id'))->searchable()->nullable(),
+ Select::make('category_id')
+ ->label('Category')
+ ->options(fn () => Category::where('is_active', true)->pluck('name', 'id'))
+ ->default(fn (): ?int => request()->integer('category_id') ?: null)
+ ->searchable()
+ ->nullable(),
StateFusionSelect::make('status')->required(),
PhoneInput::make('contact_phone')->defaultCountry(CountryCodeManager::defaultCountryIso2())->nullable(),
- TextInput::make('contact_email')->email()->maxLength(255),
- TextInput::make('city')->maxLength(100),
- CountryCodeSelect::make('country')
+ TextInput::make('contact_email')
+ ->email()
+ ->maxLength(255)
+ ->default(fn (): ?string => Filament::auth()->user()?->email),
+ Select::make('country')
->label('Country')
- ->default(fn () => CountryCodeManager::defaultCountryCode())
- ->formatStateUsing(fn ($state): ?string => CountryCodeManager::countryCodeFromLabelOrCode($state))
- ->dehydrateStateUsing(fn ($state, ?Listing $record): ?string => CountryCodeManager::normalizeStoredCountry($state ?? $record?->country)),
+ ->options(fn (): array => Country::query()
+ ->where('is_active', true)
+ ->orderBy('name')
+ ->pluck('name', 'name')
+ ->all())
+ ->default(fn (): ?string => Country::query()
+ ->where('code', CountryCodeManager::defaultCountryIso2())
+ ->value('name'))
+ ->searchable()
+ ->preload()
+ ->live()
+ ->afterStateUpdated(fn ($state, $set) => $set('city', null))
+ ->nullable(),
+ Select::make('city')
+ ->label('City')
+ ->options(function (Get $get): array {
+ $country = $get('country');
+
+ if (blank($country)) {
+ return [];
+ }
+
+ return City::query()
+ ->where('is_active', true)
+ ->whereHas('country', fn (Builder $query): Builder => $query->where('name', $country))
+ ->orderBy('name')
+ ->pluck('name', 'name')
+ ->all();
+ })
+ ->searchable()
+ ->preload()
+ ->disabled(fn (Get $get): bool => blank($get('country')))
+ ->nullable(),
Map::make('location')
->label('Location')
->visible(fn (): bool => ListingPanelHelper::googleMapsEnabled())
@@ -94,6 +162,42 @@ class ListingResource extends Resource
TextColumn::make('created_at')->dateTime()->sortable(),
])->filters([
StateFusionSelectFilter::make('status'),
+ SelectFilter::make('category_id')
+ ->label('Category')
+ ->relationship('category', 'name')
+ ->searchable()
+ ->preload(),
+ SelectFilter::make('country')
+ ->options(fn (): array => Country::query()
+ ->orderBy('name')
+ ->pluck('name', 'name')
+ ->all())
+ ->searchable(),
+ SelectFilter::make('city')
+ ->options(fn (): array => City::query()
+ ->orderBy('name')
+ ->pluck('name', 'name')
+ ->all())
+ ->searchable(),
+ TernaryFilter::make('is_featured')->label('Featured'),
+ Filter::make('created_at')
+ ->label('Created Date')
+ ->schema([
+ DatePicker::make('from')->label('From'),
+ DatePicker::make('until')->label('Until'),
+ ])
+ ->query(fn (Builder $query, array $data): Builder => $query
+ ->when($data['from'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('created_at', '>=', $date))
+ ->when($data['until'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('created_at', '<=', $date))),
+ Filter::make('price')
+ ->label('Price Range')
+ ->schema([
+ TextInput::make('min')->numeric()->label('Min'),
+ TextInput::make('max')->numeric()->label('Max'),
+ ])
+ ->query(fn (Builder $query, array $data): Builder => $query
+ ->when($data['min'] ?? null, fn (Builder $query, string $amount): Builder => $query->where('price', '>=', (float) $amount))
+ ->when($data['max'] ?? null, fn (Builder $query, string $amount): Builder => $query->where('price', '<=', (float) $amount))),
])->actions([
EditAction::make(),
Action::make('activities')
@@ -113,6 +217,7 @@ class ListingResource extends Resource
return [
'index' => Pages\ListListings::route('/'),
'create' => Pages\CreateListing::route('/create'),
+ 'quick-create' => Pages\QuickCreateListing::route('/quick-create'),
'activities' => Pages\ListListingActivities::route('/{record}/activities'),
'edit' => Pages\EditListing::route('/{record}/edit'),
];
diff --git a/Modules/Partner/Filament/Resources/ListingResource/Pages/ListListings.php b/Modules/Partner/Filament/Resources/ListingResource/Pages/ListListings.php
index 5fa8d40f6..8b4ce2225 100644
--- a/Modules/Partner/Filament/Resources/ListingResource/Pages/ListListings.php
+++ b/Modules/Partner/Filament/Resources/ListingResource/Pages/ListListings.php
@@ -1,6 +1,7 @@
label('Manuel İlan Ekle'),
+ Action::make('quickCreate')
+ ->label('Hızlı İlan Ver')
+ ->icon('heroicon-o-bolt')
+ ->color('danger')
+ ->url(ListingResource::getUrl('quick-create', shouldGuessMissingParameters: true)),
+ ];
+ }
}
diff --git a/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php b/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php
new file mode 100644
index 000000000..90afd75a3
--- /dev/null
+++ b/Modules/Partner/Filament/Resources/ListingResource/Pages/QuickCreateListing.php
@@ -0,0 +1,282 @@
+
+ */
+ public array $photos = [];
+
+ /**
+ * @var array
+ */
+ public array $categories = [];
+
+ public int $currentStep = 1;
+ public string $categorySearch = '';
+ public ?int $selectedCategoryId = null;
+ public ?int $activeParentCategoryId = null;
+ public ?int $detectedCategoryId = null;
+ public ?float $detectedConfidence = null;
+ public ?string $detectedReason = null;
+ public ?string $detectedError = null;
+
+ /**
+ * @var array
+ */
+ public array $detectedAlternatives = [];
+
+ public bool $isDetecting = false;
+
+ public function mount(): void
+ {
+ $this->loadCategories();
+ }
+
+ public function updatedPhotos(): void
+ {
+ $this->validatePhotos();
+ }
+
+ public function removePhoto(int $index): void
+ {
+ if (! isset($this->photos[$index])) {
+ return;
+ }
+
+ unset($this->photos[$index]);
+ $this->photos = array_values($this->photos);
+ }
+
+ public function goToCategoryStep(): void
+ {
+ $this->validatePhotos();
+ $this->currentStep = 2;
+
+ if (! $this->isDetecting && ! $this->detectedCategoryId) {
+ $this->detectCategoryFromImage();
+ }
+ }
+
+ public function detectCategoryFromImage(): void
+ {
+ if ($this->photos === []) {
+ return;
+ }
+
+ $this->isDetecting = true;
+ $this->detectedError = null;
+ $this->detectedReason = null;
+ $this->detectedAlternatives = [];
+
+ $result = app(QuickListingCategorySuggester::class)->suggestFromImage($this->photos[0]);
+
+ $this->isDetecting = false;
+ $this->detectedCategoryId = $result['category_id'];
+ $this->detectedConfidence = $result['confidence'];
+ $this->detectedReason = $result['reason'];
+ $this->detectedError = $result['error'];
+ $this->detectedAlternatives = $result['alternatives'];
+
+ if ($this->detectedCategoryId) {
+ $this->selectCategory($this->detectedCategoryId);
+ }
+ }
+
+ public function enterCategory(int $categoryId): void
+ {
+ if (! $this->categoryExists($categoryId)) {
+ return;
+ }
+
+ $this->activeParentCategoryId = $categoryId;
+ $this->categorySearch = '';
+ }
+
+ public function backToRootCategories(): void
+ {
+ $this->activeParentCategoryId = null;
+ $this->categorySearch = '';
+ }
+
+ public function selectCategory(int $categoryId): void
+ {
+ if (! $this->categoryExists($categoryId)) {
+ return;
+ }
+
+ $this->selectedCategoryId = $categoryId;
+ }
+
+ public function continueToManualCreate()
+ {
+ if (! $this->selectedCategoryId) {
+ return null;
+ }
+
+ $url = ListingResource::getUrl(
+ name: 'create',
+ parameters: [
+ 'category_id' => $this->selectedCategoryId,
+ 'quick' => 1,
+ ],
+ shouldGuessMissingParameters: true,
+ );
+
+ return redirect()->to($url);
+ }
+
+ /**
+ * @return array
+ */
+ public function getRootCategoriesProperty(): array
+ {
+ return collect($this->categories)
+ ->whereNull('parent_id')
+ ->values()
+ ->all();
+ }
+
+ /**
+ * @return array
+ */
+ public function getCurrentCategoriesProperty(): array
+ {
+ if (! $this->activeParentCategoryId) {
+ return [];
+ }
+
+ $search = trim((string) $this->categorySearch);
+ $all = collect($this->categories);
+ $parent = $all->firstWhere('id', $this->activeParentCategoryId);
+ $children = $all->where('parent_id', $this->activeParentCategoryId)->values();
+
+ $combined = collect();
+
+ if (is_array($parent)) {
+ $combined->push($parent);
+ }
+
+ $combined = $combined->concat($children);
+
+ return $combined
+ ->when(
+ $search !== '',
+ fn (Collection $categories): Collection => $categories->filter(
+ fn (array $category): bool => str_contains(
+ mb_strtolower($category['name']),
+ mb_strtolower($search)
+ )
+ )
+ )
+ ->values()
+ ->all();
+ }
+
+ public function getCurrentParentNameProperty(): string
+ {
+ if (! $this->activeParentCategoryId) {
+ return 'Kategori Seçimi';
+ }
+
+ $category = collect($this->categories)
+ ->firstWhere('id', $this->activeParentCategoryId);
+
+ return (string) ($category['name'] ?? 'Kategori Seçimi');
+ }
+
+ public function getSelectedCategoryNameProperty(): ?string
+ {
+ if (! $this->selectedCategoryId) {
+ return null;
+ }
+
+ $category = collect($this->categories)
+ ->firstWhere('id', $this->selectedCategoryId);
+
+ return $category['name'] ?? null;
+ }
+
+ public function categoryIconComponent(?string $icon): string
+ {
+ return match ($icon) {
+ 'car' => 'heroicon-o-truck',
+ 'laptop', 'computer' => 'heroicon-o-computer-desktop',
+ 'shirt' => 'heroicon-o-swatch',
+ 'home', 'sofa' => 'heroicon-o-home-modern',
+ 'briefcase' => 'heroicon-o-briefcase',
+ 'wrench' => 'heroicon-o-wrench-screwdriver',
+ 'football' => 'heroicon-o-trophy',
+ 'phone', 'mobile' => 'heroicon-o-device-phone-mobile',
+ default => 'heroicon-o-tag',
+ };
+ }
+
+ private function validatePhotos(): void
+ {
+ $this->validate([
+ 'photos' => [
+ 'required',
+ 'array',
+ 'min:1',
+ 'max:'.config('quick-listing.max_photo_count', 20),
+ ],
+ 'photos.*' => [
+ 'required',
+ 'image',
+ 'mimes:jpg,jpeg,png',
+ 'max:'.config('quick-listing.max_photo_size_kb', 5120),
+ ],
+ ]);
+ }
+
+ private function loadCategories(): void
+ {
+ $all = Category::query()
+ ->where('is_active', true)
+ ->orderBy('sort_order')
+ ->orderBy('name')
+ ->get(['id', 'name', 'parent_id', 'icon']);
+
+ $childrenCount = Category::query()
+ ->where('is_active', true)
+ ->selectRaw('parent_id, count(*) as aggregate')
+ ->whereNotNull('parent_id')
+ ->groupBy('parent_id')
+ ->pluck('aggregate', 'parent_id');
+
+ $this->categories = $all
+ ->map(fn (Category $category): array => [
+ 'id' => (int) $category->id,
+ 'name' => (string) $category->name,
+ 'parent_id' => $category->parent_id ? (int) $category->parent_id : null,
+ 'icon' => $category->icon,
+ 'has_children' => ((int) ($childrenCount[$category->id] ?? 0)) > 0,
+ ])
+ ->values()
+ ->all();
+ }
+
+ private function categoryExists(int $categoryId): bool
+ {
+ return collect($this->categories)
+ ->contains(fn (array $category): bool => $category['id'] === $categoryId);
+ }
+}
diff --git a/app/Http/Controllers/FavoriteController.php b/app/Http/Controllers/FavoriteController.php
new file mode 100644
index 000000000..846505bd0
--- /dev/null
+++ b/app/Http/Controllers/FavoriteController.php
@@ -0,0 +1,180 @@
+string('tab', 'listings');
+
+ if (! in_array($activeTab, ['listings', 'searches', 'sellers'], true)) {
+ $activeTab = 'listings';
+ }
+
+ $statusFilter = (string) $request->string('status', 'all');
+
+ if (! in_array($statusFilter, ['all', 'active'], true)) {
+ $statusFilter = 'all';
+ }
+
+ $selectedCategoryId = $request->integer('category');
+
+ if ($selectedCategoryId <= 0) {
+ $selectedCategoryId = null;
+ }
+
+ $user = $request->user();
+ $categories = Category::query()
+ ->where('is_active', true)
+ ->orderBy('name')
+ ->get(['id', 'name']);
+
+ $favoriteListings = null;
+ $favoriteSearches = null;
+ $favoriteSellers = null;
+
+ if ($activeTab === 'listings') {
+ $favoriteListings = $user->favoriteListings()
+ ->with(['category:id,name', 'user:id,name'])
+ ->wherePivot('created_at', '>=', now()->subYear())
+ ->when($statusFilter === 'active', fn ($query) => $query->where('status', 'active'))
+ ->when($selectedCategoryId, fn ($query) => $query->where('category_id', $selectedCategoryId))
+ ->orderByPivot('created_at', 'desc')
+ ->paginate(10)
+ ->withQueryString();
+ }
+
+ if ($activeTab === 'searches') {
+ $favoriteSearches = $user->favoriteSearches()
+ ->with('category:id,name')
+ ->latest()
+ ->paginate(10)
+ ->withQueryString();
+ }
+
+ if ($activeTab === 'sellers') {
+ $favoriteSellers = $user->favoriteSellers()
+ ->withCount([
+ 'listings as active_listings_count' => fn ($query) => $query->where('status', 'active'),
+ ])
+ ->orderByPivot('created_at', 'desc')
+ ->paginate(10)
+ ->withQueryString();
+ }
+
+ return view('favorites.index', [
+ 'activeTab' => $activeTab,
+ 'statusFilter' => $statusFilter,
+ 'selectedCategoryId' => $selectedCategoryId,
+ 'categories' => $categories,
+ 'favoriteListings' => $favoriteListings,
+ 'favoriteSearches' => $favoriteSearches,
+ 'favoriteSellers' => $favoriteSellers,
+ ]);
+ }
+
+ public function toggleListing(Request $request, Listing $listing)
+ {
+ $user = $request->user();
+ $isFavorite = $user->favoriteListings()->whereKey($listing->getKey())->exists();
+
+ if ($isFavorite) {
+ $user->favoriteListings()->detach($listing->getKey());
+
+ return back()->with('success', 'İlan favorilerden kaldırıldı.');
+ }
+
+ $user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]);
+
+ return back()->with('success', 'İlan favorilere eklendi.');
+ }
+
+ public function toggleSeller(Request $request, User $seller)
+ {
+ $user = $request->user();
+
+ if ((int) $user->getKey() === (int) $seller->getKey()) {
+ return back()->with('error', 'Kendi hesabını favorilere ekleyemezsin.');
+ }
+
+ $isFavorite = $user->favoriteSellers()->whereKey($seller->getKey())->exists();
+
+ if ($isFavorite) {
+ $user->favoriteSellers()->detach($seller->getKey());
+
+ return back()->with('success', 'Satıcı favorilerden kaldırıldı.');
+ }
+
+ $user->favoriteSellers()->syncWithoutDetaching([$seller->getKey()]);
+
+ return back()->with('success', 'Satıcı favorilere eklendi.');
+ }
+
+ public function storeSearch(Request $request)
+ {
+ $data = $request->validate([
+ 'search' => ['nullable', 'string', 'max:120'],
+ 'category_id' => ['nullable', 'integer', 'exists:categories,id'],
+ ]);
+
+ $filters = FavoriteSearch::normalizeFilters([
+ 'search' => $data['search'] ?? null,
+ 'category' => $data['category_id'] ?? null,
+ ]);
+
+ if ($filters === []) {
+ return back()->with('error', 'Favoriye eklemek için en az bir filtre seçmelisin.');
+ }
+
+ $signature = FavoriteSearch::signatureFor($filters);
+
+ $categoryName = null;
+ if (isset($filters['category'])) {
+ $categoryName = Category::query()->whereKey($filters['category'])->value('name');
+ }
+
+ $labelParts = [];
+ if (! empty($filters['search'])) {
+ $labelParts[] = '"'.$filters['search'].'"';
+ }
+ if ($categoryName) {
+ $labelParts[] = $categoryName;
+ }
+
+ $label = $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtreli arama';
+
+ $favoriteSearch = $request->user()->favoriteSearches()->firstOrCreate(
+ ['signature' => $signature],
+ [
+ 'label' => $label,
+ 'search_term' => $filters['search'] ?? null,
+ 'category_id' => $filters['category'] ?? null,
+ 'filters' => $filters,
+ ]
+ );
+
+ if (! $favoriteSearch->wasRecentlyCreated) {
+ return back()->with('success', 'Bu arama zaten favorilerinde.');
+ }
+
+ return back()->with('success', 'Arama favorilere eklendi.');
+ }
+
+ public function destroySearch(Request $request, FavoriteSearch $favoriteSearch)
+ {
+ if ((int) $favoriteSearch->user_id !== (int) $request->user()->getKey()) {
+ abort(403);
+ }
+
+ $favoriteSearch->delete();
+
+ return back()->with('success', 'Favori arama silindi.');
+ }
+}
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index 7f525c50e..4e3698432 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -16,6 +16,18 @@ class HomeController extends Controller
$listingCount = Listing::where('status', 'active')->count();
$categoryCount = Category::where('is_active', true)->count();
$userCount = User::count();
- return view('home', compact('categories', 'featuredListings', 'recentListings', 'listingCount', 'categoryCount', 'userCount'));
+ $favoriteListingIds = auth()->check()
+ ? auth()->user()->favoriteListings()->pluck('listings.id')->all()
+ : [];
+
+ return view('home', compact(
+ 'categories',
+ 'featuredListings',
+ 'recentListings',
+ 'listingCount',
+ 'categoryCount',
+ 'userCount',
+ 'favoriteListingIds',
+ ));
}
}
diff --git a/app/Models/FavoriteSearch.php b/app/Models/FavoriteSearch.php
new file mode 100644
index 000000000..fc0eecea4
--- /dev/null
+++ b/app/Models/FavoriteSearch.php
@@ -0,0 +1,48 @@
+ 'array',
+ ];
+
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+
+ public function category()
+ {
+ return $this->belongsTo(\Modules\Category\Models\Category::class);
+ }
+
+ public static function normalizeFilters(array $filters): array
+ {
+ return collect($filters)
+ ->map(fn ($value) => is_string($value) ? trim($value) : $value)
+ ->filter(fn ($value) => $value !== null && $value !== '' && $value !== [])
+ ->sortKeys()
+ ->all();
+ }
+
+ public static function signatureFor(array $filters): string
+ {
+ $normalized = static::normalizeFilters($filters);
+ $payload = json_encode($normalized);
+
+ return hash('sha256', is_string($payload) ? $payload : '');
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index 439139757..c100c0a96 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -68,6 +68,23 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata
return $this->hasMany(\Modules\Listing\Models\Listing::class);
}
+ public function favoriteListings()
+ {
+ return $this->belongsToMany(\Modules\Listing\Models\Listing::class, 'favorite_listings')
+ ->withTimestamps();
+ }
+
+ public function favoriteSellers()
+ {
+ return $this->belongsToMany(self::class, 'favorite_sellers', 'user_id', 'seller_id')
+ ->withTimestamps();
+ }
+
+ public function favoriteSearches()
+ {
+ return $this->hasMany(FavoriteSearch::class);
+ }
+
public function canImpersonate(): bool
{
return $this->hasRole('admin');
diff --git a/app/Support/QuickListingCategorySuggester.php b/app/Support/QuickListingCategorySuggester.php
new file mode 100644
index 000000000..1cc2422d3
--- /dev/null
+++ b/app/Support/QuickListingCategorySuggester.php
@@ -0,0 +1,159 @@
+,
+ * error: string|null
+ * }
+ */
+ public function suggestFromImage(UploadedFile $image): array
+ {
+ $provider = (string) config('quick-listing.ai_provider', 'openai');
+ $model = config('quick-listing.ai_model');
+ $providerKey = config("ai.providers.{$provider}.key");
+
+ if (blank($providerKey)) {
+ return [
+ 'detected' => false,
+ 'category_id' => null,
+ 'confidence' => null,
+ 'reason' => 'AI provider key is missing.',
+ 'alternatives' => [],
+ 'error' => 'AI provider key is missing.',
+ ];
+ }
+
+ $categories = Category::query()
+ ->where('is_active', true)
+ ->orderBy('sort_order')
+ ->orderBy('name')
+ ->get(['id', 'name', 'parent_id']);
+
+ if ($categories->isEmpty()) {
+ return [
+ 'detected' => false,
+ 'category_id' => null,
+ 'confidence' => null,
+ 'reason' => 'No active categories available.',
+ 'alternatives' => [],
+ 'error' => 'No active categories available.',
+ ];
+ }
+
+ $catalog = $this->buildCatalog($categories);
+ $categoryIds = $catalog->pluck('id')->values()->all();
+ $catalogText = $catalog
+ ->map(fn (array $category): string => "{$category['id']}: {$category['path']}")
+ ->implode("\n");
+
+ try {
+ $response = agent(
+ instructions: 'You are an e-commerce listing assistant. Classify the product image into the best matching category ID from the provided catalog. Never invent IDs.',
+ schema: fn (JsonSchema $schema): array => [
+ 'detected' => $schema->boolean()->required(),
+ 'category_id' => $schema->integer()->enum($categoryIds)->nullable(),
+ 'confidence' => $schema->number()->min(0)->max(1)->nullable(),
+ 'reason' => $schema->string()->required(),
+ 'alternatives' => $schema->array()->items(
+ $schema->integer()->enum($categoryIds)
+ )->max(3)->default([]),
+ ],
+ )->prompt(
+ prompt: <<filter(fn ($value): bool => is_numeric($value))
+ ->map(fn ($value): int => (int) $value)
+ ->filter(fn (int $id): bool => in_array($id, $categoryIds, true))
+ ->unique()
+ ->values()
+ ->all();
+
+ $detected = (bool) ($response['detected'] ?? false) && $categoryId !== null;
+
+ return [
+ 'detected' => $detected,
+ 'category_id' => $detected ? $categoryId : null,
+ 'confidence' => $confidence,
+ 'reason' => (string) ($response['reason'] ?? 'No reason provided.'),
+ 'alternatives' => $alternatives,
+ 'error' => null,
+ ];
+ } catch (Throwable $exception) {
+ report($exception);
+
+ return [
+ 'detected' => false,
+ 'category_id' => null,
+ 'confidence' => null,
+ 'reason' => 'Category could not be detected automatically.',
+ 'alternatives' => [],
+ 'error' => $exception->getMessage(),
+ ];
+ }
+ }
+
+ /**
+ * @param Collection $categories
+ * @return Collection
+ */
+ private function buildCatalog(Collection $categories): Collection
+ {
+ $byId = $categories->keyBy('id');
+
+ return $categories->map(function (Category $category) use ($byId): array {
+ $path = [$category->name];
+ $parentId = $category->parent_id;
+
+ while ($parentId && $byId->has($parentId)) {
+ $parent = $byId->get($parentId);
+ $path[] = $parent->name;
+ $parentId = $parent->parent_id;
+ }
+
+ return [
+ 'id' => (int) $category->id,
+ 'path' => implode(' > ', array_reverse($path)),
+ ];
+ });
+ }
+}
+
diff --git a/composer.json b/composer.json
index f71037373..ed6bb996a 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,7 @@
"filament/spatie-laravel-media-library-plugin": "^5.3",
"filament/spatie-laravel-settings-plugin": "^5.3",
"jeffgreco13/filament-breezy": "^3.2",
+ "laravel/ai": "^0.2.5",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1",
diff --git a/config/quick-listing.php b/config/quick-listing.php
new file mode 100644
index 000000000..dd713dde7
--- /dev/null
+++ b/config/quick-listing.php
@@ -0,0 +1,9 @@
+ env('QUICK_LISTING_AI_PROVIDER', 'openai'),
+ 'ai_model' => env('QUICK_LISTING_AI_MODEL', 'gpt-5.2'),
+ 'max_photo_count' => 20,
+ 'max_photo_size_kb' => 5120,
+];
+
diff --git a/database/migrations/2026_03_03_190000_create_favorites_tables.php b/database/migrations/2026_03_03_190000_create_favorites_tables.php
new file mode 100644
index 000000000..350194c75
--- /dev/null
+++ b/database/migrations/2026_03_03_190000_create_favorites_tables.php
@@ -0,0 +1,49 @@
+id();
+ $table->foreignId('user_id')->constrained()->cascadeOnDelete();
+ $table->foreignId('listing_id')->constrained('listings')->cascadeOnDelete();
+ $table->timestamps();
+
+ $table->unique(['user_id', 'listing_id']);
+ });
+
+ Schema::create('favorite_sellers', function (Blueprint $table): void {
+ $table->id();
+ $table->foreignId('user_id')->constrained()->cascadeOnDelete();
+ $table->foreignId('seller_id')->constrained('users')->cascadeOnDelete();
+ $table->timestamps();
+
+ $table->unique(['user_id', 'seller_id']);
+ });
+
+ Schema::create('favorite_searches', function (Blueprint $table): void {
+ $table->id();
+ $table->foreignId('user_id')->constrained()->cascadeOnDelete();
+ $table->string('label')->nullable();
+ $table->string('search_term')->nullable();
+ $table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
+ $table->json('filters')->nullable();
+ $table->string('signature', 64);
+ $table->timestamps();
+
+ $table->unique(['user_id', 'signature']);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('favorite_searches');
+ Schema::dropIfExists('favorite_sellers');
+ Schema::dropIfExists('favorite_listings');
+ }
+};
diff --git a/resources/views/favorites/index.blade.php b/resources/views/favorites/index.blade.php
new file mode 100644
index 000000000..c503fb290
--- /dev/null
+++ b/resources/views/favorites/index.blade.php
@@ -0,0 +1,196 @@
+@extends('app::layouts.app')
+
+@section('title', 'Favoriler')
+
+@section('content')
+
+
+
+
+
+ @if($activeTab === 'listings')
+
+
+
+
+
+
+ İlan Başlığı
+ Fiyat
+
+
+
+
+ @forelse($favoriteListings as $listing)
+ @php
+ $listingImage = $listing->getFirstMediaUrl('listing-images');
+ $priceLabel = $listing->price ? number_format((float) $listing->price, 0).' '.$listing->currency : 'Ücretsiz';
+ $meta = collect([
+ $listing->category?->name,
+ $listing->city,
+ $listing->country,
+ ])->filter()->join(' › ');
+ @endphp
+
+
+
+
+ {{ $priceLabel }}
+
+
+
+
+ @empty
+
+
+ Henüz favori ilan bulunmuyor.
+
+
+ @endforelse
+
+
+
+
+
+ * Son 1 yıl içinde favoriye eklediğiniz ilanlar listelenmektedir.
+
+
+ @if($favoriteListings?->hasPages())
+ {{ $favoriteListings->links() }}
+ @endif
+ @endif
+
+ @if($activeTab === 'searches')
+
+
Favori Aramalar
+
Kayıtlı aramalarına tek tıkla geri dön.
+
+
+ @forelse($favoriteSearches as $favoriteSearch)
+ @php
+ $searchUrl = route('listings.index', array_filter([
+ 'search' => $favoriteSearch->search_term,
+ 'category' => $favoriteSearch->category_id,
+ ]));
+ @endphp
+
+
+
{{ $favoriteSearch->label ?: 'Kayıtlı arama' }}
+
+ @if($favoriteSearch->search_term) Arama: "{{ $favoriteSearch->search_term }}" · @endif
+ @if($favoriteSearch->category) Kategori: {{ $favoriteSearch->category->name }} · @endif
+ Kaydedilme: {{ $favoriteSearch->created_at?->format('d.m.Y H:i') }}
+
+
+
+
+ @empty
+
+ Henüz favori arama eklenmedi.
+
+ @endforelse
+
+ @if($favoriteSearches?->hasPages())
+ {{ $favoriteSearches->links() }}
+ @endif
+ @endif
+
+ @if($activeTab === 'sellers')
+
+
Favori Satıcılar
+
Takip etmek istediğin satıcıları burada yönetebilirsin.
+
+
+ @forelse($favoriteSellers as $seller)
+
+
+
+ {{ strtoupper(substr((string) $seller->name, 0, 1)) }}
+
+
+
{{ $seller->name }}
+
{{ $seller->email }}
+
Aktif ilan: {{ (int) $seller->active_listings_count }}
+
+
+
+
+ @empty
+
+ Henüz favori satıcı eklenmedi.
+
+ @endforelse
+
+ @if($favoriteSellers?->hasPages())
+ {{ $favoriteSellers->links() }}
+ @endif
+ @endif
+
+
+
+@endsection
diff --git a/resources/views/filament/partner/listings/quick-create.blade.php b/resources/views/filament/partner/listings/quick-create.blade.php
new file mode 100644
index 000000000..511eaede2
--- /dev/null
+++ b/resources/views/filament/partner/listings/quick-create.blade.php
@@ -0,0 +1,262 @@
+
+
+
+
+
+
{{ $currentStep === 1 ? 'Fotoğraf' : 'Kategori Seçimi' }}
+
+
+ @for ($step = 1; $step <= 6; $step++)
+ $step <= $currentStep])>
+ @endfor
+
+
{{ $currentStep }}/6
+
+
+
+
+ @if ($currentStep === 1)
+
+
+
+ Ürün fotoğraflarını yükle
+
+ Yüklemeye başlamak için ürün fotoğraflarını
+ bu alana sürükleyip bırakın veya
+
+ Fotoğraf Seç
+
+
+
+
+
+ İ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]))
+
+
×
+ @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.
+ letgo 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)
+
+
+
+ letgo AI kategori önerdi:
+ {{ $this->selectedCategoryName }}
+ @if ($detectedConfidence)
+ (Güven: {{ number_format($detectedConfidence * 100, 0) }}%)
+ @endif
+
+
+ @else
+
+
+ letgo AI ile ilan kategorisi tespit edilemedi, lütfen kategori seçimi yapın.
+
+ @endif
+
+ @if (is_null($activeParentCategoryId))
+
+
+
+ @foreach ($this->rootCategories as $category)
+
+
+
+
+ {{ $category['name'] }}
+
+ @endforeach
+
+ @else
+
+
+
+
+
+
+
+ @forelse ($this->currentCategories as $category)
+
+
+ {{ $category['name'] }}
+
+
+ @if ($category['has_children'] && $category['id'] !== $activeParentCategoryId)
+
+
+
+ @else
+
+ @endif
+
+
+ @if ($selectedCategoryId === $category['id'])
+
+ @endif
+
+
+ @empty
+
+ Aramaya uygun kategori bulunamadı.
+
+ @endforelse
+
+ @endif
+
+ @if ($this->selectedCategoryName)
+
Seçilen kategori: {{ $this->selectedCategoryName }}
+ @endif
+
+
+ @endif
+
+
+
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php
index 446a120f4..c529631dd 100644
--- a/resources/views/home.blade.php
+++ b/resources/views/home.blade.php
@@ -138,6 +138,7 @@
$listingImage = $listing->getFirstMediaUrl('listing-images');
$priceLabel = $listing->price ? number_format((float) $listing->price, 0).' '.$listing->currency : __('messages.free');
$locationLabel = trim(collect([$listing->city, $listing->country])->filter()->join(', '));
+ $isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
@endphp
@@ -156,7 +157,16 @@
@endif
Büyük İlan
- ♡
+
+ @auth
+
+ @else
+
♡
+ @endauth
+
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php
index 94f5ca7d3..970a256cf 100644
--- a/resources/views/layouts/app.blade.php
+++ b/resources/views/layouts/app.blade.php
@@ -138,6 +138,7 @@
@auth
+
Favorilerim
Panel
+ {{ __('messages.post_listing') }}
diff --git a/routes/web.php b/routes/web.php
index b940a2044..1ec043bee 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -1,5 +1,6 @@
$redirectToPartner('filament.partner.re
Route::get('/partner/listings/create', fn () => $redirectToPartner('filament.partner.resources.listings.create'))
->name('partner.listings.create');
+Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
+ Route::get('/', [FavoriteController::class, 'index'])->name('index');
+ Route::post('/listings/{listing}/toggle', [FavoriteController::class, 'toggleListing'])->name('listings.toggle');
+ Route::post('/sellers/{seller}/toggle', [FavoriteController::class, 'toggleSeller'])->name('sellers.toggle');
+ Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store');
+ Route::delete('/searches/{favoriteSearch}', [FavoriteController::class, 'destroySearch'])->name('searches.destroy');
+});
+
require __DIR__.'/auth.php';