From d2345cbeda606ae87da2e2912bea1db98b950a66 Mon Sep 17 00:00:00 2001 From: fatihalp Date: Tue, 10 Mar 2026 21:01:30 +0300 Subject: [PATCH] Fix module seeders PSR-4 namespaces --- .../Filament/Resources/CategoryResource.php | 26 +- .../Admin/Filament/Resources/CityResource.php | 30 +- .../Filament/Resources/DistrictResource.php | 32 +- .../Resources/ListingCustomFieldResource.php | 23 +- .../Filament/Resources/ListingResource.php | 49 +-- .../Filament/Resources/LocationResource.php | 32 +- .../Admin/Filament/Resources/UserResource.php | 25 +- .../Admin/Providers/AdminServiceProvider.php | 3 +- .../Support/Filament/ResourceTableActions.php | 35 ++ .../Support/Filament/ResourceTableColumns.php | 27 ++ Modules/Category/Models/Category.php | 21 ++ .../Providers/CategoryServiceProvider.php | 3 +- .../Providers/ConversationServiceProvider.php | 6 +- .../App/Providers/DemoServiceProvider.php | 2 +- .../App/Providers/FavoriteServiceProvider.php | 6 +- .../Providers/ListingServiceProvider.php | 4 +- Modules/Location/Models/City.php | 35 +- Modules/Location/Models/Country.php | 35 ++ .../Providers/LocationServiceProvider.php | 3 +- .../App/Providers/UserServiceProvider.php | 6 +- .../Video/Providers/VideoServiceProvider.php | 2 +- README.md | 300 ++++-------------- config/modules.php | 8 +- ...03_092653_create_breezy_sessions_table.php | 9 +- ..._03_092654_alter_breezy_sessions_table.php | 31 -- ...03_03_093635_create_activity_log_table.php | 38 ++- ...add_event_column_to_activity_log_table.php | 22 -- ...atch_uuid_column_to_activity_log_table.php | 22 -- 28 files changed, 346 insertions(+), 489 deletions(-) create mode 100644 Modules/Admin/Support/Filament/ResourceTableActions.php create mode 100644 Modules/Admin/Support/Filament/ResourceTableColumns.php delete mode 100644 database/migrations/2026_03_03_092654_alter_breezy_sessions_table.php delete mode 100644 database/migrations/2026_03_03_093636_add_event_column_to_activity_log_table.php delete mode 100644 database/migrations/2026_03_03_093637_add_batch_uuid_column_to_activity_log_table.php diff --git a/Modules/Admin/Filament/Resources/CategoryResource.php b/Modules/Admin/Filament/Resources/CategoryResource.php index 09d3c9f6f..6688db7d1 100644 --- a/Modules/Admin/Filament/Resources/CategoryResource.php +++ b/Modules/Admin/Filament/Resources/CategoryResource.php @@ -1,27 +1,29 @@ required()->maxLength(255)->unique(ignoreRecord: true), TextInput::make('description')->maxLength(500), TextInput::make('icon')->maxLength(100), - Select::make('parent_id')->label('Parent Category')->options(fn () => Category::whereNull('parent_id')->pluck('name', 'id'))->nullable()->searchable(), + Select::make('parent_id')->label('Parent Category')->options(fn (): array => Category::rootIdNameOptions())->nullable()->searchable(), TextInput::make('sort_order')->numeric()->default(0), Toggle::make('is_active')->default(true), ]); @@ -39,15 +41,15 @@ class CategoryResource extends Resource public static function table(Table $table): Table { return $table->columns([ - TextColumn::make('id')->sortable(), + ResourceTableColumns::id(), TextColumn::make('name') ->searchable() - ->formatStateUsing(fn (string $state, Category $record): string => $record->parent_id === null ? $state : 'โ†ณ ' . $state) + ->formatStateUsing(fn (string $state, Category $record): string => $record->parent_id === null ? $state : 'โ†ณ '.$state) ->weight(fn (Category $record): string => $record->parent_id === null ? 'semi-bold' : 'normal'), TextColumn::make('parent.name')->label('Parent')->default('-'), TextColumn::make('children_count')->label('Subcategories'), TextColumn::make('listings_count')->label('Listings'), - IconColumn::make('is_active')->boolean(), + ResourceTableColumns::activeIcon(), TextColumn::make('sort_order')->sortable(), ])->actions([ Action::make('toggleChildren') @@ -55,11 +57,7 @@ class CategoryResource extends Resource ->icon(fn (Category $record, Pages\ListCategories $livewire): string => $livewire->hasExpandedChildren($record) ? 'heroicon-o-chevron-down' : 'heroicon-o-chevron-right') ->action(fn (Category $record, Pages\ListCategories $livewire) => $livewire->toggleChildren($record)) ->visible(fn (Category $record): bool => $record->parent_id === null && $record->children_count > 0), - EditAction::make(), - Action::make('activities') - ->icon('heroicon-o-clock') - ->url(fn (Category $record): string => static::getUrl('activities', ['record' => $record])), - DeleteAction::make(), + ...ResourceTableActions::editActivityDelete(static::class), ]); } diff --git a/Modules/Admin/Filament/Resources/CityResource.php b/Modules/Admin/Filament/Resources/CityResource.php index 0d91e0e6a..1c007bc56 100644 --- a/Modules/Admin/Filament/Resources/CityResource.php +++ b/Modules/Admin/Filament/Resources/CityResource.php @@ -1,32 +1,36 @@ columns([ - TextColumn::make('id')->sortable(), + ResourceTableColumns::id(), 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), + ResourceTableColumns::activeIcon(), + ResourceTableColumns::createdAtHidden(), ])->defaultSort('id', 'desc')->filters([ SelectFilter::make('country_id') ->label('Country') @@ -61,13 +65,7 @@ class CityResource extends Resource blank: fn (Builder $query): Builder => $query, ), 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(), - ]); + ])->actions(ResourceTableActions::editActivityDelete(static::class)); } public static function getPages(): array diff --git a/Modules/Admin/Filament/Resources/DistrictResource.php b/Modules/Admin/Filament/Resources/DistrictResource.php index 872cd5613..651e46edd 100644 --- a/Modules/Admin/Filament/Resources/DistrictResource.php +++ b/Modules/Admin/Filament/Resources/DistrictResource.php @@ -1,22 +1,21 @@ columns([ - TextColumn::make('id')->sortable(), + ResourceTableColumns::id(), 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), + ResourceTableColumns::activeIcon(), + ResourceTableColumns::createdAtHidden(), ])->defaultSort('id', 'desc')->filters([ SelectFilter::make('country_id') ->label('Country') - ->options(fn (): array => Country::query()->orderBy('name')->pluck('name', 'id')->all()) + ->options(fn (): array => Country::idNameOptions()) ->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') @@ -59,13 +63,7 @@ class DistrictResource extends Resource ->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(), - ]); + ])->actions(ResourceTableActions::editActivityDelete(static::class)); } public static function getPages(): array diff --git a/Modules/Admin/Filament/Resources/ListingCustomFieldResource.php b/Modules/Admin/Filament/Resources/ListingCustomFieldResource.php index 8e6514711..00e59cb69 100644 --- a/Modules/Admin/Filament/Resources/ListingCustomFieldResource.php +++ b/Modules/Admin/Filament/Resources/ListingCustomFieldResource.php @@ -3,12 +3,10 @@ namespace Modules\Admin\Filament\Resources; use BackedEnum; -use Filament\Actions\DeleteAction; -use Filament\Actions\EditAction; use Filament\Forms\Components\Select; use Filament\Forms\Components\TagsInput; -use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Resources\Resource; use Filament\Schemas\Schema; @@ -16,6 +14,7 @@ use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use Modules\Admin\Filament\Resources\ListingCustomFieldResource\Pages; +use Modules\Admin\Support\Filament\ResourceTableActions; use Modules\Category\Models\Category; use Modules\Listing\Models\ListingCustomField; use UnitEnum; @@ -23,8 +22,11 @@ use UnitEnum; class ListingCustomFieldResource extends Resource { protected static ?string $model = ListingCustomField::class; - protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-adjustments-horizontal'; - protected static string | UnitEnum | null $navigationGroup = 'Catalog'; + + protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-adjustments-horizontal'; + + protected static string|UnitEnum|null $navigationGroup = 'Catalog'; + protected static ?int $navigationSort = 30; public static function form(Schema $schema): Schema @@ -63,11 +65,7 @@ class ListingCustomFieldResource extends Resource ->live(), Select::make('category_id') ->label('Category') - ->options(fn (): array => Category::query() - ->where('is_active', true) - ->orderBy('name') - ->pluck('name', 'id') - ->all()) + ->options(fn (): array => Category::activeIdNameOptions()) ->searchable() ->preload() ->nullable() @@ -106,10 +104,7 @@ class ListingCustomFieldResource extends Resource TextColumn::make('sort_order')->sortable(), ]) ->defaultSort('id', 'desc') - ->actions([ - EditAction::make(), - DeleteAction::make(), - ]); + ->actions(ResourceTableActions::editDelete()); } public static function getPages(): array diff --git a/Modules/Admin/Filament/Resources/ListingResource.php b/Modules/Admin/Filament/Resources/ListingResource.php index 276b1dbad..2d38d2b18 100644 --- a/Modules/Admin/Filament/Resources/ListingResource.php +++ b/Modules/Admin/Filament/Resources/ListingResource.php @@ -1,4 +1,5 @@ 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('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), Textarea::make('description')->rows(4), TextInput::make('price') @@ -61,7 +62,7 @@ class ListingResource extends Resource ->required(), Select::make('category_id') ->label('Category') - ->options(fn () => Category::where('is_active', true)->pluck('name', 'id')) + ->options(fn (): array => Category::activeIdNameOptions()) ->searchable() ->live() ->afterStateUpdated(fn ($state, $set) => $set('custom_fields', [])) @@ -83,10 +84,7 @@ class ListingResource extends Resource Toggle::make('is_featured')->default(false), Select::make('country') ->label('Country') - ->options(fn (): array => Country::query() - ->orderBy('name') - ->pluck('name', 'name') - ->all()) + ->options(fn (): array => Country::nameOptions()) ->searchable() ->preload() ->live() @@ -94,16 +92,7 @@ class ListingResource extends Resource ->nullable(), Select::make('city') ->label('City') - ->options(function (Get $get): array { - $country = $get('country'); - - return City::query() - ->where('is_active', true) - ->when($country, fn (Builder $query, string $country): Builder => $query->whereHas('country', fn (Builder $countryQuery): Builder => $countryQuery->where('name', $country))) - ->orderBy('name') - ->pluck('name', 'name') - ->all(); - }) + ->options(fn (Get $get): array => City::nameOptions($get('country'))) ->searchable() ->preload() ->nullable(), @@ -161,16 +150,10 @@ class ListingResource extends Resource ->searchable() ->preload(), SelectFilter::make('country') - ->options(fn (): array => Country::query() - ->orderBy('name') - ->pluck('name', 'name') - ->all()) + ->options(fn (): array => Country::nameOptions()) ->searchable(), SelectFilter::make('city') - ->options(fn (): array => City::query() - ->orderBy('name') - ->pluck('name', 'name') - ->all()) + ->options(fn (): array => City::nameOptions(null, false)) ->searchable(), TernaryFilter::make('is_featured')->label('Featured'), Filter::make('created_at') @@ -197,13 +180,7 @@ class ListingResource extends Resource ->filtersFormWidth('7xl') ->persistFiltersInSession() ->defaultSort('id', 'desc') - ->actions([ - EditAction::make(), - Action::make('activities') - ->icon('heroicon-o-clock') - ->url(fn (Listing $record): string => static::getUrl('activities', ['record' => $record])), - DeleteAction::make(), - ]); + ->actions(ResourceTableActions::editActivityDelete(static::class)); } public static function getPages(): array diff --git a/Modules/Admin/Filament/Resources/LocationResource.php b/Modules/Admin/Filament/Resources/LocationResource.php index 23eb51e9e..50ce56d39 100644 --- a/Modules/Admin/Filament/Resources/LocationResource.php +++ b/Modules/Admin/Filament/Resources/LocationResource.php @@ -1,31 +1,35 @@ columns([ - TextColumn::make('id')->sortable(), + ResourceTableColumns::id(), TextColumn::make('name')->searchable()->sortable(), TextColumn::make('code')->searchable()->sortable(), TextColumn::make('phone_code'), TextColumn::make('cities_count')->counts('cities')->label('Cities')->sortable(), - IconColumn::make('is_active')->boolean(), - TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), + ResourceTableColumns::activeIcon(), + ResourceTableColumns::createdAtHidden(), ])->defaultSort('id', 'desc')->filters([ SelectFilter::make('code') ->label('Code') - ->options(fn (): array => Country::query()->orderBy('code')->pluck('code', 'code')->all()), + ->options(fn (): array => Country::codeOptions()), TernaryFilter::make('has_cities') ->label('Has cities') ->queries( @@ -60,13 +64,7 @@ class LocationResource extends Resource blank: fn (Builder $query): Builder => $query, ), TernaryFilter::make('is_active')->label('Active'), - ])->actions([ - EditAction::make(), - Action::make('activities') - ->icon('heroicon-o-clock') - ->url(fn (Country $record): string => static::getUrl('activities', ['record' => $record])), - DeleteAction::make(), - ]); + ])->actions(ResourceTableActions::editActivityDelete(static::class)); } public static function getPages(): array diff --git a/Modules/Admin/Filament/Resources/UserResource.php b/Modules/Admin/Filament/Resources/UserResource.php index 8c9295305..bc84e5d5f 100644 --- a/Modules/Admin/Filament/Resources/UserResource.php +++ b/Modules/Admin/Filament/Resources/UserResource.php @@ -1,18 +1,18 @@ columns([ - TextColumn::make('id')->sortable(), + ResourceTableColumns::id(), TextColumn::make('name')->searchable()->sortable(), TextColumn::make('email')->searchable()->sortable(), TextColumn::make('roles.name')->badge()->label('Roles'), @@ -45,14 +47,9 @@ class UserResource extends Resource TextColumn::make('created_at')->dateTime()->sortable(), ])->defaultSort('id', 'desc')->filters([ StateFusionSelectFilter::make('status'), - ])->actions([ - EditAction::make(), - Action::make('activities') - ->icon('heroicon-o-clock') - ->url(fn (User $record): string => static::getUrl('activities', ['record' => $record])), + ])->actions(ResourceTableActions::editActivityDelete(static::class, [ Impersonate::make(), - DeleteAction::make(), - ]); + ])); } public static function getPages(): array diff --git a/Modules/Admin/Providers/AdminServiceProvider.php b/Modules/Admin/Providers/AdminServiceProvider.php index 75637bf91..9a290e883 100644 --- a/Modules/Admin/Providers/AdminServiceProvider.php +++ b/Modules/Admin/Providers/AdminServiceProvider.php @@ -1,4 +1,5 @@ loadMigrationsFrom(module_path('Admin', 'database/migrations')); + $this->loadMigrationsFrom(module_path('Admin', 'Database/migrations')); } public function register(): void diff --git a/Modules/Admin/Support/Filament/ResourceTableActions.php b/Modules/Admin/Support/Filament/ResourceTableActions.php new file mode 100644 index 000000000..3c699c5c3 --- /dev/null +++ b/Modules/Admin/Support/Filament/ResourceTableActions.php @@ -0,0 +1,35 @@ +icon('heroicon-o-clock') + ->url(fn ($record): string => $resourceClass::getUrl('activities', ['record' => $record])); + } +} diff --git a/Modules/Admin/Support/Filament/ResourceTableColumns.php b/Modules/Admin/Support/Filament/ResourceTableColumns.php new file mode 100644 index 000000000..3684ed23e --- /dev/null +++ b/Modules/Admin/Support/Filament/ResourceTableColumns.php @@ -0,0 +1,27 @@ +sortable(); + } + + public static function activeIcon(string $name = 'is_active', string $label = 'Active'): IconColumn + { + return IconColumn::make($name)->label($label)->boolean(); + } + + public static function createdAtHidden(string $name = 'created_at'): TextColumn + { + return TextColumn::make($name) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true); + } +} diff --git a/Modules/Category/Models/Category.php b/Modules/Category/Models/Category.php index f2725131c..f47906efd 100644 --- a/Modules/Category/Models/Category.php +++ b/Modules/Category/Models/Category.php @@ -1,4 +1,5 @@ 'boolean']; public function getActivitylogOptions(): LogOptions @@ -103,6 +105,25 @@ class Category extends Model ->get(['id', 'name']); } + public static function activeIdNameOptions(): array + { + return static::query() + ->active() + ->ordered() + ->pluck('name', 'id') + ->all(); + } + + public static function rootIdNameOptions(): array + { + return static::query() + ->active() + ->whereNull('parent_id') + ->ordered() + ->pluck('name', 'id') + ->all(); + } + public static function themePills(int $limit = 8): Collection { return static::query() diff --git a/Modules/Category/Providers/CategoryServiceProvider.php b/Modules/Category/Providers/CategoryServiceProvider.php index 08ca08c17..a00b42bde 100644 --- a/Modules/Category/Providers/CategoryServiceProvider.php +++ b/Modules/Category/Providers/CategoryServiceProvider.php @@ -1,4 +1,5 @@ loadMigrationsFrom(module_path($this->moduleName, 'database/migrations')); + $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/migrations')); $this->loadRoutesFrom(module_path($this->moduleName, 'routes/web.php')); $this->loadViewsFrom(module_path($this->moduleName, 'resources/views'), 'category'); } diff --git a/Modules/Conversation/App/Providers/ConversationServiceProvider.php b/Modules/Conversation/App/Providers/ConversationServiceProvider.php index 50fb1ef9e..93f5ef416 100644 --- a/Modules/Conversation/App/Providers/ConversationServiceProvider.php +++ b/Modules/Conversation/App/Providers/ConversationServiceProvider.php @@ -9,7 +9,7 @@ class ConversationServiceProvider extends ServiceProvider { public function boot(): void { - $this->loadMigrationsFrom(module_path('Conversation', 'database/migrations')); + $this->loadMigrationsFrom(module_path('Conversation', 'Database/migrations')); $this->loadRoutesFrom(module_path('Conversation', 'routes/web.php')); $this->loadViewsFrom(module_path('Conversation', 'resources/views'), 'conversation'); @@ -18,7 +18,5 @@ class ConversationServiceProvider extends ServiceProvider }); } - public function register(): void - { - } + public function register(): void {} } diff --git a/Modules/Demo/App/Providers/DemoServiceProvider.php b/Modules/Demo/App/Providers/DemoServiceProvider.php index 114470827..00e063159 100644 --- a/Modules/Demo/App/Providers/DemoServiceProvider.php +++ b/Modules/Demo/App/Providers/DemoServiceProvider.php @@ -25,7 +25,7 @@ class DemoServiceProvider extends ServiceProvider public function boot(): void { $this->guardConfiguration(); - $this->loadMigrationsFrom(module_path('Demo', 'database/migrations')); + $this->loadMigrationsFrom(module_path('Demo', 'Database/migrations')); $this->loadRoutesFrom(module_path('Demo', 'routes/web.php')); } diff --git a/Modules/Favorite/App/Providers/FavoriteServiceProvider.php b/Modules/Favorite/App/Providers/FavoriteServiceProvider.php index 3e456d801..7b340c71a 100644 --- a/Modules/Favorite/App/Providers/FavoriteServiceProvider.php +++ b/Modules/Favorite/App/Providers/FavoriteServiceProvider.php @@ -8,12 +8,10 @@ class FavoriteServiceProvider extends ServiceProvider { public function boot(): void { - $this->loadMigrationsFrom(module_path('Favorite', 'database/migrations')); + $this->loadMigrationsFrom(module_path('Favorite', 'Database/migrations')); $this->loadRoutesFrom(module_path('Favorite', 'routes/web.php')); $this->loadViewsFrom(module_path('Favorite', 'resources/views'), 'favorite'); } - public function register(): void - { - } + public function register(): void {} } diff --git a/Modules/Listing/Providers/ListingServiceProvider.php b/Modules/Listing/Providers/ListingServiceProvider.php index 276501e03..1adbb53ca 100644 --- a/Modules/Listing/Providers/ListingServiceProvider.php +++ b/Modules/Listing/Providers/ListingServiceProvider.php @@ -1,4 +1,5 @@ loadViewsFrom(module_path($this->moduleName, 'resources/views'), $this->moduleNameLower); - $this->loadMigrationsFrom(module_path($this->moduleName, 'database/migrations')); + $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/migrations')); $this->loadRoutesFrom(module_path($this->moduleName, 'routes/web.php')); } diff --git a/Modules/Location/Models/City.php b/Modules/Location/Models/City.php index 8cff54c82..15e364c11 100644 --- a/Modules/Location/Models/City.php +++ b/Modules/Location/Models/City.php @@ -1,6 +1,8 @@ 'boolean']; + public function scopeActive(Builder $query): Builder + { + return $query->where('is_active', true); + } + public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() @@ -20,6 +28,29 @@ class City extends Model ->dontSubmitEmptyLogs(); } - public function country() { return $this->belongsTo(Country::class); } - public function districts() { return $this->hasMany(District::class); } + public function country() + { + return $this->belongsTo(Country::class); + } + + public function districts() + { + return $this->hasMany(District::class); + } + + public static function nameOptions(?string $countryName = null, bool $onlyActive = true): array + { + return static::query() + ->when($onlyActive, fn (Builder $query): Builder => $query->active()) + ->when( + $countryName && trim($countryName) !== '', + fn (Builder $query): Builder => $query->whereHas( + 'country', + fn (Builder $countryQuery): Builder => $countryQuery->where('name', trim($countryName)), + ), + ) + ->orderBy('name') + ->pluck('name', 'name') + ->all(); + } } diff --git a/Modules/Location/Models/Country.php b/Modules/Location/Models/Country.php index 6ccb3754c..9225c9b18 100644 --- a/Modules/Location/Models/Country.php +++ b/Modules/Location/Models/Country.php @@ -1,6 +1,8 @@ 'boolean']; + public function scopeActive(Builder $query): Builder + { + return $query->where('is_active', true); + } + public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() @@ -24,4 +32,31 @@ class Country extends Model { return $this->hasMany(City::class); } + + public static function idNameOptions(bool $onlyActive = false): array + { + return static::query() + ->when($onlyActive, fn (Builder $query): Builder => $query->active()) + ->orderBy('name') + ->pluck('name', 'id') + ->all(); + } + + public static function codeOptions(bool $onlyActive = false): array + { + return static::query() + ->when($onlyActive, fn (Builder $query): Builder => $query->active()) + ->orderBy('code') + ->pluck('code', 'code') + ->all(); + } + + public static function nameOptions(bool $onlyActive = false): array + { + return static::query() + ->when($onlyActive, fn (Builder $query): Builder => $query->active()) + ->orderBy('name') + ->pluck('name', 'name') + ->all(); + } } diff --git a/Modules/Location/Providers/LocationServiceProvider.php b/Modules/Location/Providers/LocationServiceProvider.php index 5a4d1206a..ccba4507c 100644 --- a/Modules/Location/Providers/LocationServiceProvider.php +++ b/Modules/Location/Providers/LocationServiceProvider.php @@ -1,4 +1,5 @@ loadMigrationsFrom(module_path($this->moduleName, 'database/migrations')); + $this->loadMigrationsFrom(module_path($this->moduleName, 'Database/migrations')); $this->loadRoutesFrom(module_path($this->moduleName, 'routes/web.php')); } diff --git a/Modules/User/App/Providers/UserServiceProvider.php b/Modules/User/App/Providers/UserServiceProvider.php index 40f564b8e..be05f8d1c 100644 --- a/Modules/User/App/Providers/UserServiceProvider.php +++ b/Modules/User/App/Providers/UserServiceProvider.php @@ -8,12 +8,10 @@ class UserServiceProvider extends ServiceProvider { public function boot(): void { - $this->loadMigrationsFrom(module_path('User', 'database/migrations')); + $this->loadMigrationsFrom(module_path('User', 'Database/migrations')); $this->loadRoutesFrom(module_path('User', 'routes/web.php')); $this->loadViewsFrom(module_path('User', 'resources/views'), 'user'); } - public function register(): void - { - } + public function register(): void {} } diff --git a/Modules/Video/Providers/VideoServiceProvider.php b/Modules/Video/Providers/VideoServiceProvider.php index dd3b3aa5c..f6ddea3ea 100644 --- a/Modules/Video/Providers/VideoServiceProvider.php +++ b/Modules/Video/Providers/VideoServiceProvider.php @@ -9,7 +9,7 @@ class VideoServiceProvider extends ServiceProvider public function boot(): void { $this->loadViewsFrom(module_path('Video', 'resources/views'), 'video'); - $this->loadMigrationsFrom(module_path('Video', 'database/migrations')); + $this->loadMigrationsFrom(module_path('Video', 'Database/migrations')); } public function register(): void diff --git a/README.md b/README.md index bd3651cb3..7a3fcb92b 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,76 @@ # OpenClassify -A modern classified ads platform built with Laravel 12, FilamentPHP v5, and Laravel Modules โ€” similar to Letgo and Sahibinden. +OpenClassify is a modular classifieds marketplace built with Laravel 12 and Filament v5. -## Features +## Core Stack -- ๐Ÿ›๏ธ **Classified Listings** โ€” Browse, search, and post ads across categories -- ๐Ÿ—‚๏ธ **Categories** โ€” Hierarchical categories with icons -- ๐Ÿ“ **Locations** โ€” Country and city management -- ๐Ÿ‘ค **User Profiles** โ€” Manage your listings and account -- ๐Ÿ” **Admin Panel** โ€” Full control via FilamentPHP v5 at `/admin` -- ๐Ÿงญ **Frontend Panel** โ€” Authenticated users manage listings, profile, videos, favorites, and inbox at `/panel` -- ๐Ÿงช **Demo Mode** โ€” Per-visitor PostgreSQL schema provisioning with seeded data and automatic cleanup -- ๐ŸŒ **10 Languages** โ€” English, Turkish, Arabic, German, French, Spanish, Portuguese, Russian, Chinese, Japanese -- ๐Ÿณ **Docker Ready** โ€” One-command production and development setup -- โ˜๏ธ **GitHub Codespaces** โ€” Zero-config cloud development +- Laravel 12 +- FilamentPHP v5 +- `nwidart/laravel-modules` +- Blade + Tailwind + Vite +- Spatie Permission +- Laravel Reverb + Echo (realtime chat) +## Modules -## Tech Stack +All business features live in `Modules/*` (routes, services, models, resources, views, seeders). -| Layer | Technology | -|-------|-----------| -| Framework | Laravel 12 | -| Admin UI | FilamentPHP v5 | -| Modules | nWidart/laravel-modules v11 | -| Auth/Roles | Spatie Laravel Permission | -| Frontend | Blade + TailwindCSS + Vite | -| Database | PostgreSQL (required for demo mode) | -| Cache/Queue | Database or Redis | - -## Quick Start (Docker) +Create a new module: ```bash -# Clone the repository -git clone https://github.com/openclassify/openclassify.git -cd openclassify +php artisan module:make ModuleName +``` -# Copy environment file +Enable it in `modules_statuses.json`. + +## Quick Start + +### Docker + +```bash cp .env.example .env - -# Start with Docker Compose (production-like) docker compose up -d - -# The application will be available at http://localhost:8000 ``` -### Default Accounts +App URLs: -| Role | Email | Password | -|------|-------|----------| -| Admin | a@a.com | 236330 | -| Member | b@b.com | 36330 | +- Frontend: `http://localhost:8000` +- Admin: `http://localhost:8000/admin` +- Panel: `http://localhost:8000/panel` -These accounts are seeded by `Modules\User\Database\Seeders\AuthUserSeeder`. In demo mode, demo preparation still auto-logs the visitor into the schema-local admin account. +### Local -**Admin Panel:** http://localhost:8000/admin -**Frontend Panel:** http://localhost:8000/panel - ---- - -## Development Setup - -### Option 1: GitHub Codespaces (Zero Config) - -1. Click **Code โ†’ Codespaces โ†’ New codespace** on GitHub -2. Wait for the environment to build (~2 minutes) -3. The app starts automatically at port 8000 - -### Option 2: Docker Development +Requirements: PHP 8.2+, Composer, Node 18+, database server. ```bash -# Start development environment with hot reload -docker compose -f docker-compose.dev.yml up -d - -# View logs -docker compose -f docker-compose.dev.yml logs -f app -``` - -### Option 3: Local (PHP + Node) - -**Requirements:** PHP 8.2+, Composer, Node 18+, PostgreSQL for demo mode - -```bash -# Install dependencies composer install npm install - -# Setup environment cp .env.example .env php artisan key:generate - -# Database (SQLite for quick start) -touch database/database.sqlite php artisan migrate php artisan db:seed - -# Start all services (server + queue + vite) composer run dev ``` +## Seeded Accounts + +| Role | Email | Password | +|------|-------|----------| +| Admin | `a@a.com` | `236330` | +| Member | `b@b.com` | `36330` | + ## Demo Mode -Demo mode is designed for isolated visitor sessions. When enabled, each visitor can provision a private temporary marketplace backed by its own PostgreSQL schema. +Demo mode provisions a temporary, per-visitor marketplace schema. -### Requirements +Requirements: - `DB_CONNECTION=pgsql` - `DEMO=1` -- database-backed session / cache / queue drivers are supported and will stay on the public schema via `pgsql_public` -If `DEMO=1` is set while the app is not using PostgreSQL, the application fails fast during boot. - -### Runtime Behavior - -- On the first guest homepage visit, the primary visible CTA is a single large `Prepare Demo` button. -- The homepage shows how long the temporary demo will live before automatic deletion. -- Clicking `Prepare Demo` provisions a visitor-specific schema, runs `migrate` and `db:seed`, and logs the visitor into the seeded admin account. -- The same browser reuses its active demo instead of creating duplicate schemas. -- Demo lifetime defaults to `360` minutes from explicit prepare / reopen time. -- Expired demos are removed by `demo:cleanup`, which is scheduled hourly. - -### Environment +Minimal `.env`: ```env -DB_CONNECTION=pgsql DEMO=1 DEMO_TTL_MINUTES=360 DEMO_SCHEMA_PREFIX=demo_ @@ -131,195 +79,71 @@ DEMO_LOGIN_EMAIL=a@a.com DEMO_PUBLIC_SCHEMA=public ``` -### Commands +Commands: ```bash -php artisan migrate --force -php artisan db:seed --force php artisan demo:prepare php artisan demo:cleanup ``` -### Panels +Notes: -| Panel | URL | Access | -|-------|-----|--------| -| Admin | `/admin` | Users with `admin` role | -| Frontend Panel | `/panel` | All authenticated users | +- First guest homepage shows only `Prepare Demo`. +- `Prepare Demo` creates/reuses a private schema and logs in seeded admin. +- Expired demos are cleaned up automatically (hourly schedule). -### Roles (Spatie Permission) +## Realtime Chat (Reverb) -| Role | Access | -|------|--------| -| `admin` | Full admin panel access | - ---- - -## Realtime Chat (Laravel Reverb) - -This project already uses Laravel Reverb + Echo for inbox and listing chat realtime updates. - -### 1. Environment - -Set these values in `.env`: +Set `.env`: ```env BROADCAST_CONNECTION=reverb - -REVERB_APP_ID=480227 -REVERB_APP_KEY=your_key -REVERB_APP_SECRET=your_secret +REVERB_APP_ID=app_id +REVERB_APP_KEY=app_key +REVERB_APP_SECRET=app_secret REVERB_HOST=localhost REVERB_PORT=8080 REVERB_SCHEME=http REVERB_SERVER_HOST=0.0.0.0 REVERB_SERVER_PORT=8080 - VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" VITE_REVERB_HOST="${REVERB_HOST}" VITE_REVERB_PORT="${REVERB_PORT}" VITE_REVERB_SCHEME="${REVERB_SCHEME}" ``` -### 2. Start Services - -Use one command: +Start: ```bash composer run dev ``` -Or run separately: +Channel strategy: -```bash -php artisan serve -php artisan reverb:start --host=0.0.0.0 --port=8080 -php artisan queue:listen --tries=1 --timeout=0 -npm run dev -``` +- private channel: `users.{id}.inbox` +- events: `InboxMessageCreated`, `ConversationReadUpdated` -### 3. How It Works in This Codebase - -- Private channel: `users.{id}.inbox` -- Channel authorization: `Modules/Conversation/App/Providers/ConversationServiceProvider.php` -- Broadcast events: - - `InboxMessageCreated` (`.inbox.message.created`) - - `ConversationReadUpdated` (`.inbox.read.updated`) -- Frontend subscriptions: `Modules/Conversation/resources/assets/js/conversation.js` -- Echo bootstrap: `resources/js/echo.js` - -### 4. Quick Verification - -1. Open two different authenticated sessions (for example `a@a.com` and `b@b.com`). -2. Go to `/panel/inbox` in both sessions. -3. Send a message from one session. -4. Confirm in the other session: - - thread updates instantly, - - inbox ordering/unread state updates, - - header inbox badge updates. - -### 5. Troubleshooting - -- No realtime updates: - - check `php artisan reverb:start` is running, - - check Vite is running (`npm run dev`) and assets are rebuilt. -- Private channel auth fails (`403`): - - verify user is authenticated in the same browser/session. -- WebSocket connection fails: - - verify `REVERB_HOST/PORT/SCHEME` and matching `VITE_REVERB_*` values, - - run `php artisan optimize:clear` after env changes. - ---- - -## Code Contributors - -

- - OpenClassify Logo - -

- -OpenClassify is a modular open source classified platform built with Laravel. - -- Website: [openclassify.com](https://openclassify.com) -- Package: [openclassify/openclassify](https://packagist.org/packages/openclassify/openclassify) - -This project is maintained and improved by its contributors. - -

- - OpenClassify Contributors - -

- -## Creating a New Module - -```bash -php artisan module:make ModuleName -``` - -Then add to `modules_statuses.json`: -```json -{ - "ModuleName": true -} -``` - ---- - -## Adding a Filament Resource to Admin Panel - -Resources are auto-discovered from `Modules/Admin/Filament/Resources/`. - ---- - -## Language Support - -Languages are in `lang/{locale}/messages.php`. To add a new language: - -1. Create `lang/{locale}/messages.php` -2. Switch language via: `GET /lang/{locale}` - -Supported locales: `en`, `tr`, `ar`, `de`, `fr`, `es`, `pt`, `ru`, `zh`, `ja` - ---- - -## Running Tests +## Test and Build ```bash php artisan test +php artisan optimize:clear +php artisan view:cache ``` ---- - -## Production Deployment - -### Environment Variables - -```env -APP_ENV=production -APP_DEBUG=false -APP_URL=https://yourdomain.com - -DB_CONNECTION=mysql -DB_HOST=your-db-host -DB_DATABASE=openclassify -DB_USERNAME=openclassify -DB_PASSWORD=your-secure-password - -REDIS_HOST=your-redis-host -CACHE_STORE=redis -SESSION_DRIVER=redis -QUEUE_CONNECTION=redis -``` - -### Post-Deploy Commands +## Production Checklist ```bash php artisan migrate --force -php artisan db:seed --force # Only on first deploy +php artisan db:seed --force +php artisan storage:link php artisan config:cache php artisan route:cache php artisan view:cache -php artisan storage:link ``` + +## Contributors + +- Website: [openclassify.com](https://openclassify.com) +- Package: [openclassify/openclassify](https://packagist.org/packages/openclassify/openclassify) +- Contributors: [GitHub graph](https://github.com/openclassify/openclassify/graphs/contributors) diff --git a/config/modules.php b/config/modules.php index 9b627eefc..ae8e08ec8 100644 --- a/config/modules.php +++ b/config/modules.php @@ -146,10 +146,10 @@ return [ // config/ 'config' => ['path' => 'config', 'generate' => true], - // database/ - 'factory' => ['path' => 'database/factories', 'generate' => true], - 'migration' => ['path' => 'database/migrations', 'generate' => true], - 'seeder' => ['path' => 'database/seeders', 'generate' => true], + // Database/ + 'factory' => ['path' => 'Database/Factories', 'generate' => true], + 'migration' => ['path' => 'Database/migrations', 'generate' => true], + 'seeder' => ['path' => 'Database/Seeders', 'generate' => true], // lang/ 'lang' => ['path' => 'lang', 'generate' => false], diff --git a/database/migrations/2026_03_03_092653_create_breezy_sessions_table.php b/database/migrations/2026_03_03_092653_create_breezy_sessions_table.php index 586ff6650..abf695379 100644 --- a/database/migrations/2026_03_03_092653_create_breezy_sessions_table.php +++ b/database/migrations/2026_03_03_092653_create_breezy_sessions_table.php @@ -6,25 +6,20 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { - public function up() + public function up(): void { Schema::create('breezy_sessions', function (Blueprint $table) { $table->id(); $table->morphs('authenticatable'); $table->string('panel_id')->nullable(); - $table->string('guard')->nullable(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->timestamp('expires_at')->nullable(); $table->text('two_factor_secret')->nullable(); $table->text('two_factor_recovery_codes')->nullable(); $table->timestamp('two_factor_confirmed_at')->nullable(); $table->timestamps(); }); - } - public function down() + public function down(): void { Schema::dropIfExists('breezy_sessions'); } diff --git a/database/migrations/2026_03_03_092654_alter_breezy_sessions_table.php b/database/migrations/2026_03_03_092654_alter_breezy_sessions_table.php deleted file mode 100644 index f433b19cc..000000000 --- a/database/migrations/2026_03_03_092654_alter_breezy_sessions_table.php +++ /dev/null @@ -1,31 +0,0 @@ -dropColumn([ - 'guard', - 'ip_address', - 'user_agent', - 'expires_at', - ]); - }); - } - - public function down(): void - { - Schema::table('breezy_sessions', function (Blueprint $table) { - $table->after('panel_id', function (BluePrint $table) { - $table->string('guard')->nullable(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->timestamp('expires_at')->nullable(); - }); - }); - } -}; diff --git a/database/migrations/2026_03_03_093635_create_activity_log_table.php b/database/migrations/2026_03_03_093635_create_activity_log_table.php index 7c05bc892..0fee00b14 100644 --- a/database/migrations/2026_03_03_093635_create_activity_log_table.php +++ b/database/migrations/2026_03_03_093635_create_activity_log_table.php @@ -1,27 +1,31 @@ create(config('activitylog.table_name'), function (Blueprint $table) { - $table->bigIncrements('id'); - $table->string('log_name')->nullable(); - $table->text('description'); - $table->nullableMorphs('subject', 'subject'); - $table->nullableMorphs('causer', 'causer'); - $table->json('properties')->nullable(); - $table->timestamps(); - $table->index('log_name'); - }); + Schema::connection(config('activitylog.database_connection')) + ->create(config('activitylog.table_name'), function (Blueprint $table): void { + $table->bigIncrements('id'); + $table->string('log_name')->nullable(); + $table->text('description'); + $table->nullableMorphs('subject', 'subject'); + $table->string('event')->nullable(); + $table->nullableMorphs('causer', 'causer'); + $table->json('properties')->nullable(); + $table->uuid('batch_uuid')->nullable(); + $table->timestamps(); + $table->index('log_name'); + }); } - public function down() + public function down(): void { - Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name')); + Schema::connection(config('activitylog.database_connection')) + ->dropIfExists(config('activitylog.table_name')); } -} +}; diff --git a/database/migrations/2026_03_03_093636_add_event_column_to_activity_log_table.php b/database/migrations/2026_03_03_093636_add_event_column_to_activity_log_table.php deleted file mode 100644 index 7b797fd5e..000000000 --- a/database/migrations/2026_03_03_093636_add_event_column_to_activity_log_table.php +++ /dev/null @@ -1,22 +0,0 @@ -table(config('activitylog.table_name'), function (Blueprint $table) { - $table->string('event')->nullable()->after('subject_type'); - }); - } - - public function down() - { - Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { - $table->dropColumn('event'); - }); - } -} diff --git a/database/migrations/2026_03_03_093637_add_batch_uuid_column_to_activity_log_table.php b/database/migrations/2026_03_03_093637_add_batch_uuid_column_to_activity_log_table.php deleted file mode 100644 index 8f7db6654..000000000 --- a/database/migrations/2026_03_03_093637_add_batch_uuid_column_to_activity_log_table.php +++ /dev/null @@ -1,22 +0,0 @@ -table(config('activitylog.table_name'), function (Blueprint $table) { - $table->uuid('batch_uuid')->nullable()->after('properties'); - }); - } - - public function down() - { - Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { - $table->dropColumn('batch_uuid'); - }); - } -}