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 - -
-
-
-
-
-
-
-
-