diff --git a/.gemini/CUSTOM_INSTRUCTIONS.md b/.gemini/CUSTOM_INSTRUCTIONS.md index 031b84595..a98c16fcd 100644 --- a/.gemini/CUSTOM_INSTRUCTIONS.md +++ b/.gemini/CUSTOM_INSTRUCTIONS.md @@ -4,4 +4,4 @@ Act as a Senior Laravel & FilamentPHP Architect. Refactor the attached code as a 3. Refactoring: Move all database logic into Models and extract repetitive Filament code into dedicated Helper classes. Identify and fix any existing logical errors. 4. Database: Consolidate migrations into a single file per table or topic (e.g., users, cache, jobs) to reduce the overall number of migration files. 5. Modularity: Use the `laravel-modules` package to encapsulate all features, routing, and Filament resources strictly inside their respective modules. -6. Frontend: Optimize and reduce the CSS footprint while maintaining the exact same visual output. +6. Frontend: Optimize and reduce the CSS footprint while maintaining the exact same visual output. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..031b84595 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +Act as a Senior Laravel & FilamentPHP Architect. Refactor the attached code as a greenfield project adhering to the following strict constraints: +1. Architecture: Enforce strict SOLID principles, prioritize brevity, and completely ignore backward compatibility. +2. Cleanup: Remove all legacy code, comments, tests, and PHPDocs. +3. Refactoring: Move all database logic into Models and extract repetitive Filament code into dedicated Helper classes. Identify and fix any existing logical errors. +4. Database: Consolidate migrations into a single file per table or topic (e.g., users, cache, jobs) to reduce the overall number of migration files. +5. Modularity: Use the `laravel-modules` package to encapsulate all features, routing, and Filament resources strictly inside their respective modules. +6. Frontend: Optimize and reduce the CSS footprint while maintaining the exact same visual output. diff --git a/Modules/Admin/Filament/Pages/ManageGeneralSettings.php b/Modules/Admin/Filament/Pages/ManageGeneralSettings.php index eeb5ee084..0c10a91ad 100644 --- a/Modules/Admin/Filament/Pages/ManageGeneralSettings.php +++ b/Modules/Admin/Filament/Pages/ManageGeneralSettings.php @@ -33,16 +33,51 @@ class ManageGeneralSettings extends SettingsPage protected static ?int $navigationSort = 1; + protected function mutateFormDataBeforeFill(array $data): array + { + $defaults = $this->defaultFormData(); + + return [ + 'site_name' => filled($data['site_name'] ?? null) ? $data['site_name'] : $defaults['site_name'], + 'site_description' => filled($data['site_description'] ?? null) ? $data['site_description'] : $defaults['site_description'], + 'home_slides' => $this->normalizeHomeSlides($data['home_slides'] ?? $defaults['home_slides']), + 'site_logo' => $data['site_logo'] ?? null, + 'sender_name' => filled($data['sender_name'] ?? null) ? $data['sender_name'] : $defaults['sender_name'], + 'sender_email' => filled($data['sender_email'] ?? null) ? $data['sender_email'] : $defaults['sender_email'], + 'default_language' => filled($data['default_language'] ?? null) ? $data['default_language'] : $defaults['default_language'], + 'default_country_code' => filled($data['default_country_code'] ?? null) ? $data['default_country_code'] : $defaults['default_country_code'], + 'currencies' => $this->normalizeCurrencies($data['currencies'] ?? $defaults['currencies']), + 'linkedin_url' => filled($data['linkedin_url'] ?? null) ? $data['linkedin_url'] : $defaults['linkedin_url'], + 'instagram_url' => filled($data['instagram_url'] ?? null) ? $data['instagram_url'] : $defaults['instagram_url'], + 'whatsapp' => filled($data['whatsapp'] ?? null) ? $data['whatsapp'] : $defaults['whatsapp'], + 'enable_google_maps' => (bool) ($data['enable_google_maps'] ?? $defaults['enable_google_maps']), + 'google_maps_api_key' => $data['google_maps_api_key'] ?? null, + 'enable_google_login' => (bool) ($data['enable_google_login'] ?? $defaults['enable_google_login']), + 'google_client_id' => $data['google_client_id'] ?? null, + 'google_client_secret' => $data['google_client_secret'] ?? null, + 'enable_facebook_login' => (bool) ($data['enable_facebook_login'] ?? $defaults['enable_facebook_login']), + 'facebook_client_id' => $data['facebook_client_id'] ?? null, + 'facebook_client_secret' => $data['facebook_client_secret'] ?? null, + 'enable_apple_login' => (bool) ($data['enable_apple_login'] ?? $defaults['enable_apple_login']), + 'apple_client_id' => $data['apple_client_id'] ?? null, + 'apple_client_secret' => $data['apple_client_secret'] ?? null, + ]; + } + public function form(Schema $schema): Schema { + $defaults = $this->defaultFormData(); + return $schema ->components([ TextInput::make('site_name') ->label('Site Adı') + ->default($defaults['site_name']) ->required() ->maxLength(255), Textarea::make('site_description') ->label('Site Açıklaması') + ->default($defaults['site_description']) ->rows(3) ->maxLength(500), Repeater::make('home_slides') @@ -70,7 +105,7 @@ class ManageGeneralSettings extends SettingsPage ->required() ->maxLength(120), ]) - ->default($this->defaultHomeSlides()) + ->default($defaults['home_slides']) ->minItems(1) ->collapsible() ->reorderableWithButtons() @@ -86,26 +121,30 @@ class ManageGeneralSettings extends SettingsPage ->visibility('public'), TextInput::make('sender_name') ->label('Gönderici Adı') + ->default($defaults['sender_name']) ->required() ->maxLength(120), TextInput::make('sender_email') ->label('Gönderici E-postası') ->email() + ->default($defaults['sender_email']) ->required() ->maxLength(255), Select::make('default_language') ->label('Varsayılan Dil') ->options($this->localeOptions()) + ->default($defaults['default_language']) ->required() ->searchable(), CountryCodeSelect::make('default_country_code') ->label('Varsayılan Ülke') - ->default('+90') + ->default($defaults['default_country_code']) ->required() ->helperText('Panel formlarında varsayılan ülke olarak kullanılır.'), TagsInput::make('currencies') ->label('Para Birimleri') ->placeholder('TRY') + ->default($defaults['currencies']) ->helperText('TRY, USD, EUR gibi 3 harfli para birimi kodları ekleyin.') ->required() ->rules(['array', 'min:1']) @@ -114,22 +153,25 @@ class ManageGeneralSettings extends SettingsPage TextInput::make('linkedin_url') ->label('LinkedIn URL') ->url() + ->default($defaults['linkedin_url']) ->nullable() ->maxLength(255), TextInput::make('instagram_url') ->label('Instagram URL') ->url() + ->default($defaults['instagram_url']) ->nullable() ->maxLength(255), PhoneInput::make('whatsapp') ->label('WhatsApp') ->defaultCountry(CountryCodeManager::defaultCountryIso2()) + ->default($defaults['whatsapp']) ->nullable() ->formatAsYouType() ->helperText('Uluslararası format kullanın. Örnek: +905551112233'), Toggle::make('enable_google_maps') ->label('Google Maps Aktif') - ->default(false), + ->default($defaults['enable_google_maps']), TextInput::make('google_maps_api_key') ->label('Google Maps API Anahtarı') ->password() @@ -139,7 +181,7 @@ class ManageGeneralSettings extends SettingsPage ->helperText('İlan formlarındaki harita alanlarını açmak için gereklidir.'), Toggle::make('enable_google_login') ->label('Google ile Giriş Aktif') - ->default(false), + ->default($defaults['enable_google_login']), TextInput::make('google_client_id') ->label('Google Client ID') ->nullable() @@ -152,7 +194,7 @@ class ManageGeneralSettings extends SettingsPage ->maxLength(255), Toggle::make('enable_facebook_login') ->label('Facebook ile Giriş Aktif') - ->default(false), + ->default($defaults['enable_facebook_login']), TextInput::make('facebook_client_id') ->label('Facebook Client ID') ->nullable() @@ -165,7 +207,7 @@ class ManageGeneralSettings extends SettingsPage ->maxLength(255), Toggle::make('enable_apple_login') ->label('Apple ile Giriş Aktif') - ->default(false), + ->default($defaults['enable_apple_login']), TextInput::make('apple_client_id') ->label('Apple Client ID') ->nullable() @@ -179,6 +221,30 @@ class ManageGeneralSettings extends SettingsPage ]); } + private function defaultFormData(): array + { + $siteName = (string) config('app.name', 'OpenClassify'); + $siteHost = parse_url((string) config('app.url', 'https://oc2.test'), PHP_URL_HOST) ?: 'oc2.test'; + + return [ + 'site_name' => $siteName, + 'site_description' => 'Alim satim icin hizli ve guvenli ilan platformu.', + 'home_slides' => $this->defaultHomeSlides(), + 'sender_name' => $siteName, + 'sender_email' => (string) config('mail.from.address', 'info@' . $siteHost), + 'default_language' => in_array(config('app.locale'), array_keys($this->localeOptions()), true) ? (string) config('app.locale') : 'tr', + 'default_country_code' => CountryCodeManager::normalizeCountryCode(config('app.default_country_code', '+90')), + 'currencies' => $this->normalizeCurrencies(config('app.currencies', ['TRY'])), + 'linkedin_url' => 'https://www.linkedin.com/company/openclassify', + 'instagram_url' => 'https://www.instagram.com/openclassify', + 'whatsapp' => '+905551112233', + 'enable_google_maps' => false, + 'enable_google_login' => false, + 'enable_facebook_login' => false, + 'enable_apple_login' => false, + ]; + } + private function localeOptions(): array { $labels = [ diff --git a/Modules/Admin/Filament/Resources/CategoryResource.php b/Modules/Admin/Filament/Resources/CategoryResource.php index 1344c6241..e8114ba3b 100644 --- a/Modules/Admin/Filament/Resources/CategoryResource.php +++ b/Modules/Admin/Filament/Resources/CategoryResource.php @@ -45,7 +45,7 @@ class CategoryResource extends Resource TextColumn::make('listings_count')->counts('listings')->label('Listings'), IconColumn::make('is_active')->boolean(), TextColumn::make('sort_order')->sortable(), - ])->actions([ + ])->defaultSort('id', 'desc')->actions([ EditAction::make(), Action::make('activities') ->icon('heroicon-o-clock') diff --git a/Modules/Admin/Filament/Resources/CityResource.php b/Modules/Admin/Filament/Resources/CityResource.php index 877f3f15d..f60eb0517 100644 --- a/Modules/Admin/Filament/Resources/CityResource.php +++ b/Modules/Admin/Filament/Resources/CityResource.php @@ -46,7 +46,7 @@ class CityResource extends Resource TextColumn::make('districts_count')->counts('districts')->label('Districts')->sortable(), IconColumn::make('is_active')->boolean(), TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), - ])->filters([ + ])->defaultSort('id', 'desc')->filters([ SelectFilter::make('country_id') ->label('Country') ->relationship('country', 'name') diff --git a/Modules/Admin/Filament/Resources/DistrictResource.php b/Modules/Admin/Filament/Resources/DistrictResource.php index 4577a12c4..f0ca451bb 100644 --- a/Modules/Admin/Filament/Resources/DistrictResource.php +++ b/Modules/Admin/Filament/Resources/DistrictResource.php @@ -48,7 +48,7 @@ class DistrictResource extends Resource TextColumn::make('city.country.name')->label('Country'), IconColumn::make('is_active')->boolean(), TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), - ])->filters([ + ])->defaultSort('id', 'desc')->filters([ SelectFilter::make('country_id') ->label('Country') ->options(fn (): array => Country::query()->orderBy('name')->pluck('name', 'id')->all()) diff --git a/Modules/Admin/Filament/Resources/ListingCustomFieldResource.php b/Modules/Admin/Filament/Resources/ListingCustomFieldResource.php index 55742f18a..8e6514711 100644 --- a/Modules/Admin/Filament/Resources/ListingCustomFieldResource.php +++ b/Modules/Admin/Filament/Resources/ListingCustomFieldResource.php @@ -105,7 +105,7 @@ class ListingCustomFieldResource extends Resource IconColumn::make('is_active')->boolean()->label('Active'), TextColumn::make('sort_order')->sortable(), ]) - ->defaultSort('sort_order') + ->defaultSort('id', 'desc') ->actions([ EditAction::make(), DeleteAction::make(), diff --git a/Modules/Admin/Filament/Resources/ListingResource.php b/Modules/Admin/Filament/Resources/ListingResource.php index 6105ff6b2..4f2155627 100644 --- a/Modules/Admin/Filament/Resources/ListingResource.php +++ b/Modules/Admin/Filament/Resources/ListingResource.php @@ -194,6 +194,7 @@ class ListingResource extends Resource ->filtersFormColumns(3) ->filtersFormWidth('7xl') ->persistFiltersInSession() + ->defaultSort('id', 'desc') ->actions([ EditAction::make(), Action::make('activities') diff --git a/Modules/Admin/Filament/Resources/LocationResource.php b/Modules/Admin/Filament/Resources/LocationResource.php index de4cb56be..cdc679c20 100644 --- a/Modules/Admin/Filament/Resources/LocationResource.php +++ b/Modules/Admin/Filament/Resources/LocationResource.php @@ -46,7 +46,7 @@ class LocationResource extends Resource TextColumn::make('cities_count')->counts('cities')->label('Cities')->sortable(), IconColumn::make('is_active')->boolean(), TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), - ])->filters([ + ])->defaultSort('id', 'desc')->filters([ TernaryFilter::make('is_active')->label('Active'), ])->actions([ EditAction::make(), diff --git a/Modules/Admin/Filament/Resources/UserResource.php b/Modules/Admin/Filament/Resources/UserResource.php index e47db0ad9..8c9295305 100644 --- a/Modules/Admin/Filament/Resources/UserResource.php +++ b/Modules/Admin/Filament/Resources/UserResource.php @@ -43,7 +43,7 @@ class UserResource extends Resource TextColumn::make('roles.name')->badge()->label('Roles'), StateFusionSelectColumn::make('status'), TextColumn::make('created_at')->dateTime()->sortable(), - ])->filters([ + ])->defaultSort('id', 'desc')->filters([ StateFusionSelectFilter::make('status'), ])->actions([ EditAction::make(), diff --git a/Modules/Category/Http/Controllers/CategoryController.php b/Modules/Category/Http/Controllers/CategoryController.php index 64212c121..60b51b6b6 100644 --- a/Modules/Category/Http/Controllers/CategoryController.php +++ b/Modules/Category/Http/Controllers/CategoryController.php @@ -3,7 +3,6 @@ namespace Modules\Category\Http\Controllers; use App\Http\Controllers\Controller; use Modules\Category\Models\Category; -use Modules\Listing\Models\Listing; use Modules\Theme\Support\ThemeManager; class CategoryController extends Controller @@ -18,22 +17,4 @@ class CategoryController extends Controller return view($this->themes->view('category', 'index'), compact('categories')); } - - public function show(Category $category) - { - $category->loadMissing([ - 'children' => fn ($query) => $query->active()->ordered(), - ]); - - $categoryIds = $category->descendantAndSelfIds()->all(); - - $listings = Listing::query() - ->where('status', 'active') - ->whereIn('category_id', $categoryIds) - ->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 1951abdb3..5f66ef1cf 100644 --- a/Modules/Category/Models/Category.php +++ b/Modules/Category/Models/Category.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Collection; +use Modules\Listing\Models\Listing; use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\Traits\LogsActivity; @@ -78,6 +79,58 @@ class Category extends Model ->get(); } + public static function listingDirectory(?int $selectedCategoryId): array + { + $categories = static::query() + ->active() + ->ordered() + ->get(['id', 'name', 'parent_id']); + + $activeListingCounts = Listing::query() + ->active() + ->whereNotNull('category_id') + ->selectRaw('category_id, count(*) as aggregate') + ->groupBy('category_id') + ->pluck('aggregate', 'category_id') + ->map(fn ($count): int => (int) $count); + + return [ + 'categories' => static::buildListingDirectoryTree($categories, $activeListingCounts), + 'selectedCategory' => $selectedCategoryId + ? $categories->firstWhere('id', $selectedCategoryId) + : null, + 'filterIds' => static::listingFilterIds($selectedCategoryId, $categories), + ]; + } + + public static function listingFilterIds(?int $selectedCategoryId, ?Collection $categories = null): ?array + { + if (! $selectedCategoryId) { + return null; + } + + if ($categories instanceof Collection) { + $selectedCategory = $categories->firstWhere('id', $selectedCategoryId); + + if (! $selectedCategory instanceof self) { + return []; + } + + return static::descendantAndSelfIdsFromCollection($selectedCategoryId, $categories); + } + + $selectedCategory = static::query() + ->active() + ->whereKey($selectedCategoryId) + ->first(['id']); + + if (! $selectedCategory) { + return []; + } + + return $selectedCategory->descendantAndSelfIds()->all(); + } + public function descendantAndSelfIds(): Collection { $ids = collect([(int) $this->getKey()]); @@ -127,4 +180,54 @@ class Category extends Model { return $this->hasMany(\Modules\Listing\Models\Listing::class)->where('status', 'active'); } + + private static function buildListingDirectoryTree(Collection $categories, Collection $activeListingCounts, ?int $parentId = null): Collection + { + return $categories + ->filter(fn (Category $category): bool => $parentId === null + ? $category->parent_id === null + : (int) $category->parent_id === $parentId) + ->values() + ->map(function (Category $category) use ($categories, $activeListingCounts): Category { + $children = static::buildListingDirectoryTree($categories, $activeListingCounts, (int) $category->getKey()); + $directActiveListingsCount = (int) $activeListingCounts->get((int) $category->getKey(), 0); + $activeListingTotal = $directActiveListingsCount + $children->sum( + fn (Category $child): int => (int) $child->getAttribute('active_listing_total') + ); + + $category->setRelation('children', $children); + $category->setAttribute('direct_active_listings_count', $directActiveListingsCount); + $category->setAttribute('active_listing_total', $activeListingTotal); + + return $category; + }) + ->values(); + } + + private static function descendantAndSelfIdsFromCollection(int $selectedCategoryId, Collection $categories): array + { + $ids = collect([$selectedCategoryId]); + $frontier = collect([$selectedCategoryId]); + + while ($frontier->isNotEmpty()) { + $children = $categories + ->filter(fn (Category $category): bool => $category->parent_id !== null && in_array((int) $category->parent_id, $frontier->all(), true)) + ->pluck('id') + ->map(fn ($id): int => (int) $id) + ->values(); + + if ($children->isEmpty()) { + break; + } + + $ids = $ids + ->merge($children) + ->unique() + ->values(); + + $frontier = $children; + } + + return $ids->all(); + } } diff --git a/Modules/Category/resources/views/index.blade.php b/Modules/Category/resources/views/index.blade.php index e7c719c45..ad08cf022 100644 --- a/Modules/Category/resources/views/index.blade.php +++ b/Modules/Category/resources/views/index.blade.php @@ -4,7 +4,7 @@
{{ $category->children->count() }} subcategories
diff --git a/Modules/Category/resources/views/show.blade.php b/Modules/Category/resources/views/show.blade.php deleted file mode 100644 index e827be93e..000000000 --- a/Modules/Category/resources/views/show.blade.php +++ /dev/null @@ -1,33 +0,0 @@ -@extends('app::layouts.app') -@section('content') -{{ $category->description }}
@endif -{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}
- View → -No listings in this category yet.
- @endforelse -{{ $category->children->count() }} subcategories
diff --git a/Modules/Category/resources/views/themes/default/show.blade.php b/Modules/Category/resources/views/themes/default/show.blade.php deleted file mode 100644 index e827be93e..000000000 --- a/Modules/Category/resources/views/themes/default/show.blade.php +++ /dev/null @@ -1,33 +0,0 @@ -@extends('app::layouts.app') -@section('content') -{{ $category->description }}
@endif -{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}
- View → -No listings in this category yet.
- @endforelse -{{ $category->children->count() }} subcategories
diff --git a/Modules/Category/resources/views/themes/otoplus/show.blade.php b/Modules/Category/resources/views/themes/otoplus/show.blade.php deleted file mode 100644 index e827be93e..000000000 --- a/Modules/Category/resources/views/themes/otoplus/show.blade.php +++ /dev/null @@ -1,33 +0,0 @@ -@extends('app::layouts.app') -@section('content') -{{ $category->description }}
@endif -{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}
- View → -No listings in this category yet.
- @endforelse -Konum Tercihi
- +Location
+Tarayıcı konumuna göre ülke ve şehir otomatik seçilebilir.
+Auto-select country and city from your browser location.
{{ $siteDescription }}