Fix module seeders PSR-4 namespaces

This commit is contained in:
fatihalp 2026-03-10 21:01:30 +03:00
parent 6ea371e372
commit d2345cbeda
28 changed files with 346 additions and 489 deletions

View File

@ -1,26 +1,28 @@
<?php <?php
namespace Modules\Admin\Filament\Resources; namespace Modules\Admin\Filament\Resources;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Modules\Admin\Filament\Resources\CategoryResource\Pages; use Modules\Admin\Filament\Resources\CategoryResource\Pages;
use Modules\Admin\Support\Filament\ResourceTableActions;
use Modules\Admin\Support\Filament\ResourceTableColumns;
use Modules\Category\Models\Category; use Modules\Category\Models\Category;
use UnitEnum; use UnitEnum;
class CategoryResource extends Resource class CategoryResource extends Resource
{ {
protected static ?string $model = Category::class; protected static ?string $model = Category::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-tag'; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-tag';
protected static string|UnitEnum|null $navigationGroup = 'Catalog'; protected static string|UnitEnum|null $navigationGroup = 'Catalog';
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema
@ -30,7 +32,7 @@ class CategoryResource extends Resource
TextInput::make('slug')->required()->maxLength(255)->unique(ignoreRecord: true), TextInput::make('slug')->required()->maxLength(255)->unique(ignoreRecord: true),
TextInput::make('description')->maxLength(500), TextInput::make('description')->maxLength(500),
TextInput::make('icon')->maxLength(100), 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), TextInput::make('sort_order')->numeric()->default(0),
Toggle::make('is_active')->default(true), Toggle::make('is_active')->default(true),
]); ]);
@ -39,7 +41,7 @@ class CategoryResource extends Resource
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table->columns([ return $table->columns([
TextColumn::make('id')->sortable(), ResourceTableColumns::id(),
TextColumn::make('name') TextColumn::make('name')
->searchable() ->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)
@ -47,7 +49,7 @@ class CategoryResource extends Resource
TextColumn::make('parent.name')->label('Parent')->default('-'), TextColumn::make('parent.name')->label('Parent')->default('-'),
TextColumn::make('children_count')->label('Subcategories'), TextColumn::make('children_count')->label('Subcategories'),
TextColumn::make('listings_count')->label('Listings'), TextColumn::make('listings_count')->label('Listings'),
IconColumn::make('is_active')->boolean(), ResourceTableColumns::activeIcon(),
TextColumn::make('sort_order')->sortable(), TextColumn::make('sort_order')->sortable(),
])->actions([ ])->actions([
Action::make('toggleChildren') 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') ->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)) ->action(fn (Category $record, Pages\ListCategories $livewire) => $livewire->toggleChildren($record))
->visible(fn (Category $record): bool => $record->parent_id === null && $record->children_count > 0), ->visible(fn (Category $record): bool => $record->parent_id === null && $record->children_count > 0),
EditAction::make(), ...ResourceTableActions::editActivityDelete(static::class),
Action::make('activities')
->icon('heroicon-o-clock')
->url(fn (Category $record): string => static::getUrl('activities', ['record' => $record])),
DeleteAction::make(),
]); ]);
} }

View File

@ -1,32 +1,36 @@
<?php <?php
namespace Modules\Admin\Filament\Resources; namespace Modules\Admin\Filament\Resources;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Modules\Admin\Filament\Resources\CityResource\Pages; use Modules\Admin\Filament\Resources\CityResource\Pages;
use Modules\Admin\Support\Filament\ResourceTableActions;
use Modules\Admin\Support\Filament\ResourceTableColumns;
use Modules\Location\Models\City; use Modules\Location\Models\City;
use UnitEnum; use UnitEnum;
class CityResource extends Resource class CityResource extends Resource
{ {
protected static ?string $model = City::class; protected static ?string $model = City::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2'; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-building-office-2';
protected static string|UnitEnum|null $navigationGroup = 'Location'; protected static string|UnitEnum|null $navigationGroup = 'Location';
protected static ?string $label = 'City'; protected static ?string $label = 'City';
protected static ?string $pluralLabel = 'Cities'; protected static ?string $pluralLabel = 'Cities';
protected static ?int $navigationSort = 3; protected static ?int $navigationSort = 3;
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema
@ -41,12 +45,12 @@ class CityResource extends Resource
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table->columns([ return $table->columns([
TextColumn::make('id')->sortable(), ResourceTableColumns::id(),
TextColumn::make('name')->searchable()->sortable(), TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('country.name')->label('Country')->searchable()->sortable(), TextColumn::make('country.name')->label('Country')->searchable()->sortable(),
TextColumn::make('districts_count')->counts('districts')->label('Districts')->sortable(), TextColumn::make('districts_count')->counts('districts')->label('Districts')->sortable(),
IconColumn::make('is_active')->boolean(), ResourceTableColumns::activeIcon(),
TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), ResourceTableColumns::createdAtHidden(),
])->defaultSort('id', 'desc')->filters([ ])->defaultSort('id', 'desc')->filters([
SelectFilter::make('country_id') SelectFilter::make('country_id')
->label('Country') ->label('Country')
@ -61,13 +65,7 @@ class CityResource extends Resource
blank: fn (Builder $query): Builder => $query, blank: fn (Builder $query): Builder => $query,
), ),
TernaryFilter::make('is_active')->label('Active'), TernaryFilter::make('is_active')->label('Active'),
])->actions([ ])->actions(ResourceTableActions::editActivityDelete(static::class));
EditAction::make(),
Action::make('activities')
->icon('heroicon-o-clock')
->url(fn (City $record): string => static::getUrl('activities', ['record' => $record])),
DeleteAction::make(),
]);
} }
public static function getPages(): array public static function getPages(): array

View File

@ -1,22 +1,21 @@
<?php <?php
namespace Modules\Admin\Filament\Resources; namespace Modules\Admin\Filament\Resources;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Modules\Admin\Filament\Resources\DistrictResource\Pages; use Modules\Admin\Filament\Resources\DistrictResource\Pages;
use Modules\Admin\Support\Filament\ResourceTableActions;
use Modules\Admin\Support\Filament\ResourceTableColumns;
use Modules\Location\Models\Country; use Modules\Location\Models\Country;
use Modules\Location\Models\District; use Modules\Location\Models\District;
use UnitEnum; use UnitEnum;
@ -24,10 +23,15 @@ use UnitEnum;
class DistrictResource extends Resource class DistrictResource extends Resource
{ {
protected static ?string $model = District::class; protected static ?string $model = District::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-map'; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-map';
protected static string|UnitEnum|null $navigationGroup = 'Location'; protected static string|UnitEnum|null $navigationGroup = 'Location';
protected static ?string $label = 'District'; protected static ?string $label = 'District';
protected static ?string $pluralLabel = 'Districts'; protected static ?string $pluralLabel = 'Districts';
protected static ?int $navigationSort = 4; protected static ?int $navigationSort = 4;
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema
@ -42,16 +46,16 @@ class DistrictResource extends Resource
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table->columns([ return $table->columns([
TextColumn::make('id')->sortable(), ResourceTableColumns::id(),
TextColumn::make('name')->searchable()->sortable(), TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('city.name')->label('City')->searchable()->sortable(), TextColumn::make('city.name')->label('City')->searchable()->sortable(),
TextColumn::make('city.country.name')->label('Country'), TextColumn::make('city.country.name')->label('Country'),
IconColumn::make('is_active')->boolean(), ResourceTableColumns::activeIcon(),
TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), ResourceTableColumns::createdAtHidden(),
])->defaultSort('id', 'desc')->filters([ ])->defaultSort('id', 'desc')->filters([
SelectFilter::make('country_id') SelectFilter::make('country_id')
->label('Country') ->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)))), ->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') SelectFilter::make('city_id')
->label('City') ->label('City')
@ -59,13 +63,7 @@ class DistrictResource extends Resource
->searchable() ->searchable()
->preload(), ->preload(),
TernaryFilter::make('is_active')->label('Active'), TernaryFilter::make('is_active')->label('Active'),
])->actions([ ])->actions(ResourceTableActions::editActivityDelete(static::class));
EditAction::make(),
Action::make('activities')
->icon('heroicon-o-clock')
->url(fn (District $record): string => static::getUrl('activities', ['record' => $record])),
DeleteAction::make(),
]);
} }
public static function getPages(): array public static function getPages(): array

View File

@ -3,12 +3,10 @@
namespace Modules\Admin\Filament\Resources; namespace Modules\Admin\Filament\Resources;
use BackedEnum; use BackedEnum;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
@ -16,6 +14,7 @@ use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Modules\Admin\Filament\Resources\ListingCustomFieldResource\Pages; use Modules\Admin\Filament\Resources\ListingCustomFieldResource\Pages;
use Modules\Admin\Support\Filament\ResourceTableActions;
use Modules\Category\Models\Category; use Modules\Category\Models\Category;
use Modules\Listing\Models\ListingCustomField; use Modules\Listing\Models\ListingCustomField;
use UnitEnum; use UnitEnum;
@ -23,8 +22,11 @@ use UnitEnum;
class ListingCustomFieldResource extends Resource class ListingCustomFieldResource extends Resource
{ {
protected static ?string $model = ListingCustomField::class; protected static ?string $model = ListingCustomField::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-adjustments-horizontal'; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-adjustments-horizontal';
protected static string|UnitEnum|null $navigationGroup = 'Catalog'; protected static string|UnitEnum|null $navigationGroup = 'Catalog';
protected static ?int $navigationSort = 30; protected static ?int $navigationSort = 30;
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema
@ -63,11 +65,7 @@ class ListingCustomFieldResource extends Resource
->live(), ->live(),
Select::make('category_id') Select::make('category_id')
->label('Category') ->label('Category')
->options(fn (): array => Category::query() ->options(fn (): array => Category::activeIdNameOptions())
->where('is_active', true)
->orderBy('name')
->pluck('name', 'id')
->all())
->searchable() ->searchable()
->preload() ->preload()
->nullable() ->nullable()
@ -106,10 +104,7 @@ class ListingCustomFieldResource extends Resource
TextColumn::make('sort_order')->sortable(), TextColumn::make('sort_order')->sortable(),
]) ])
->defaultSort('id', 'desc') ->defaultSort('id', 'desc')
->actions([ ->actions(ResourceTableActions::editDelete());
EditAction::make(),
DeleteAction::make(),
]);
} }
public static function getPages(): array public static function getPages(): array

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Modules\Admin\Filament\Resources; namespace Modules\Admin\Filament\Resources;
use A909M\FilamentStateFusion\Forms\Components\StateFusionSelect; use A909M\FilamentStateFusion\Forms\Components\StateFusionSelect;
@ -7,9 +8,6 @@ use A909M\FilamentStateFusion\Tables\Filters\StateFusionSelectFilter;
use App\Support\CountryCodeManager; use App\Support\CountryCodeManager;
use BackedEnum; use BackedEnum;
use Cheesegrits\FilamentGoogleMaps\Fields\Map; use Cheesegrits\FilamentGoogleMaps\Fields\Map;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\SpatieMediaLibraryFileUpload; use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
@ -30,6 +28,7 @@ use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Modules\Admin\Filament\Resources\ListingResource\Pages; use Modules\Admin\Filament\Resources\ListingResource\Pages;
use Modules\Admin\Support\Filament\ResourceTableActions;
use Modules\Category\Models\Category; use Modules\Category\Models\Category;
use Modules\Listing\Models\Listing; use Modules\Listing\Models\Listing;
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder; use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
@ -43,7 +42,9 @@ use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
class ListingResource extends Resource class ListingResource extends Resource
{ {
protected static ?string $model = Listing::class; protected static ?string $model = Listing::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-clipboard-document-list'; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-clipboard-document-list';
protected static string|UnitEnum|null $navigationGroup = 'Catalog'; protected static string|UnitEnum|null $navigationGroup = 'Catalog';
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema
@ -61,7 +62,7 @@ class ListingResource extends Resource
->required(), ->required(),
Select::make('category_id') Select::make('category_id')
->label('Category') ->label('Category')
->options(fn () => Category::where('is_active', true)->pluck('name', 'id')) ->options(fn (): array => Category::activeIdNameOptions())
->searchable() ->searchable()
->live() ->live()
->afterStateUpdated(fn ($state, $set) => $set('custom_fields', [])) ->afterStateUpdated(fn ($state, $set) => $set('custom_fields', []))
@ -83,10 +84,7 @@ class ListingResource extends Resource
Toggle::make('is_featured')->default(false), Toggle::make('is_featured')->default(false),
Select::make('country') Select::make('country')
->label('Country') ->label('Country')
->options(fn (): array => Country::query() ->options(fn (): array => Country::nameOptions())
->orderBy('name')
->pluck('name', 'name')
->all())
->searchable() ->searchable()
->preload() ->preload()
->live() ->live()
@ -94,16 +92,7 @@ class ListingResource extends Resource
->nullable(), ->nullable(),
Select::make('city') Select::make('city')
->label('City') ->label('City')
->options(function (Get $get): array { ->options(fn (Get $get): array => City::nameOptions($get('country')))
$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();
})
->searchable() ->searchable()
->preload() ->preload()
->nullable(), ->nullable(),
@ -161,16 +150,10 @@ class ListingResource extends Resource
->searchable() ->searchable()
->preload(), ->preload(),
SelectFilter::make('country') SelectFilter::make('country')
->options(fn (): array => Country::query() ->options(fn (): array => Country::nameOptions())
->orderBy('name')
->pluck('name', 'name')
->all())
->searchable(), ->searchable(),
SelectFilter::make('city') SelectFilter::make('city')
->options(fn (): array => City::query() ->options(fn (): array => City::nameOptions(null, false))
->orderBy('name')
->pluck('name', 'name')
->all())
->searchable(), ->searchable(),
TernaryFilter::make('is_featured')->label('Featured'), TernaryFilter::make('is_featured')->label('Featured'),
Filter::make('created_at') Filter::make('created_at')
@ -197,13 +180,7 @@ class ListingResource extends Resource
->filtersFormWidth('7xl') ->filtersFormWidth('7xl')
->persistFiltersInSession() ->persistFiltersInSession()
->defaultSort('id', 'desc') ->defaultSort('id', 'desc')
->actions([ ->actions(ResourceTableActions::editActivityDelete(static::class));
EditAction::make(),
Action::make('activities')
->icon('heroicon-o-clock')
->url(fn (Listing $record): string => static::getUrl('activities', ['record' => $record])),
DeleteAction::make(),
]);
} }
public static function getPages(): array public static function getPages(): array

View File

@ -1,31 +1,35 @@
<?php <?php
namespace Modules\Admin\Filament\Resources; namespace Modules\Admin\Filament\Resources;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Modules\Admin\Filament\Resources\LocationResource\Pages; use Modules\Admin\Filament\Resources\LocationResource\Pages;
use Modules\Admin\Support\Filament\ResourceTableActions;
use Modules\Admin\Support\Filament\ResourceTableColumns;
use Modules\Location\Models\Country; use Modules\Location\Models\Country;
use UnitEnum; use UnitEnum;
class LocationResource extends Resource class LocationResource extends Resource
{ {
protected static ?string $model = Country::class; protected static ?string $model = Country::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-globe-alt'; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-globe-alt';
protected static string|UnitEnum|null $navigationGroup = 'Location'; protected static string|UnitEnum|null $navigationGroup = 'Location';
protected static ?string $label = 'Country'; protected static ?string $label = 'Country';
protected static ?string $pluralLabel = 'Countries'; protected static ?string $pluralLabel = 'Countries';
protected static ?int $navigationSort = 2; protected static ?int $navigationSort = 2;
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema
@ -41,17 +45,17 @@ class LocationResource extends Resource
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table->columns([ return $table->columns([
TextColumn::make('id')->sortable(), ResourceTableColumns::id(),
TextColumn::make('name')->searchable()->sortable(), TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('code')->searchable()->sortable(), TextColumn::make('code')->searchable()->sortable(),
TextColumn::make('phone_code'), TextColumn::make('phone_code'),
TextColumn::make('cities_count')->counts('cities')->label('Cities')->sortable(), TextColumn::make('cities_count')->counts('cities')->label('Cities')->sortable(),
IconColumn::make('is_active')->boolean(), ResourceTableColumns::activeIcon(),
TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), ResourceTableColumns::createdAtHidden(),
])->defaultSort('id', 'desc')->filters([ ])->defaultSort('id', 'desc')->filters([
SelectFilter::make('code') SelectFilter::make('code')
->label('Code') ->label('Code')
->options(fn (): array => Country::query()->orderBy('code')->pluck('code', 'code')->all()), ->options(fn (): array => Country::codeOptions()),
TernaryFilter::make('has_cities') TernaryFilter::make('has_cities')
->label('Has cities') ->label('Has cities')
->queries( ->queries(
@ -60,13 +64,7 @@ class LocationResource extends Resource
blank: fn (Builder $query): Builder => $query, blank: fn (Builder $query): Builder => $query,
), ),
TernaryFilter::make('is_active')->label('Active'), TernaryFilter::make('is_active')->label('Active'),
])->actions([ ])->actions(ResourceTableActions::editActivityDelete(static::class));
EditAction::make(),
Action::make('activities')
->icon('heroicon-o-clock')
->url(fn (Country $record): string => static::getUrl('activities', ['record' => $record])),
DeleteAction::make(),
]);
} }
public static function getPages(): array public static function getPages(): array

View File

@ -1,18 +1,18 @@
<?php <?php
namespace Modules\Admin\Filament\Resources; namespace Modules\Admin\Filament\Resources;
use A909M\FilamentStateFusion\Tables\Columns\StateFusionSelectColumn; use A909M\FilamentStateFusion\Tables\Columns\StateFusionSelectColumn;
use A909M\FilamentStateFusion\Tables\Filters\StateFusionSelectFilter; use A909M\FilamentStateFusion\Tables\Filters\StateFusionSelectFilter;
use Modules\User\App\Models\User;
use BackedEnum; use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table; use Filament\Tables\Table;
use Modules\Admin\Filament\Resources\UserResource\Pages; use Modules\Admin\Filament\Resources\UserResource\Pages;
use Modules\Admin\Support\Filament\ResourceTableActions;
use Modules\Admin\Support\Filament\ResourceTableColumns;
use Modules\User\App\Models\User;
use Modules\User\App\Support\Filament\UserFormFields; use Modules\User\App\Support\Filament\UserFormFields;
use STS\FilamentImpersonate\Actions\Impersonate; use STS\FilamentImpersonate\Actions\Impersonate;
use UnitEnum; use UnitEnum;
@ -20,7 +20,9 @@ use UnitEnum;
class UserResource extends Resource class UserResource extends Resource
{ {
protected static ?string $model = User::class; protected static ?string $model = User::class;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-users'; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-users';
protected static string|UnitEnum|null $navigationGroup = 'User Management'; protected static string|UnitEnum|null $navigationGroup = 'User Management';
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema
@ -37,7 +39,7 @@ class UserResource extends Resource
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table->columns([ return $table->columns([
TextColumn::make('id')->sortable(), ResourceTableColumns::id(),
TextColumn::make('name')->searchable()->sortable(), TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('email')->searchable()->sortable(), TextColumn::make('email')->searchable()->sortable(),
TextColumn::make('roles.name')->badge()->label('Roles'), TextColumn::make('roles.name')->badge()->label('Roles'),
@ -45,14 +47,9 @@ class UserResource extends Resource
TextColumn::make('created_at')->dateTime()->sortable(), TextColumn::make('created_at')->dateTime()->sortable(),
])->defaultSort('id', 'desc')->filters([ ])->defaultSort('id', 'desc')->filters([
StateFusionSelectFilter::make('status'), StateFusionSelectFilter::make('status'),
])->actions([ ])->actions(ResourceTableActions::editActivityDelete(static::class, [
EditAction::make(),
Action::make('activities')
->icon('heroicon-o-clock')
->url(fn (User $record): string => static::getUrl('activities', ['record' => $record])),
Impersonate::make(), Impersonate::make(),
DeleteAction::make(), ]));
]);
} }
public static function getPages(): array public static function getPages(): array

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Modules\Admin\Providers; namespace Modules\Admin\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -7,7 +8,7 @@ class AdminServiceProvider extends ServiceProvider
{ {
public function boot(): void public function boot(): void
{ {
$this->loadMigrationsFrom(module_path('Admin', 'database/migrations')); $this->loadMigrationsFrom(module_path('Admin', 'Database/migrations'));
} }
public function register(): void public function register(): void

View File

@ -0,0 +1,35 @@
<?php
namespace Modules\Admin\Support\Filament;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
final class ResourceTableActions
{
public static function editDelete(): array
{
return [
EditAction::make(),
DeleteAction::make(),
];
}
public static function editActivityDelete(string $resourceClass, array $afterActivity = []): array
{
return [
EditAction::make(),
self::activities($resourceClass),
...$afterActivity,
DeleteAction::make(),
];
}
public static function activities(string $resourceClass): Action
{
return Action::make('activities')
->icon('heroicon-o-clock')
->url(fn ($record): string => $resourceClass::getUrl('activities', ['record' => $record]));
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Modules\Admin\Support\Filament;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
final class ResourceTableColumns
{
public static function id(string $name = 'id'): TextColumn
{
return TextColumn::make($name)->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);
}
}

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Modules\Category\Models; namespace Modules\Category\Models;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@ -32,6 +33,7 @@ class Category extends Model
]; ];
protected $fillable = ['name', 'slug', 'description', 'icon', 'parent_id', 'level', 'sort_order', 'is_active']; protected $fillable = ['name', 'slug', 'description', 'icon', 'parent_id', 'level', 'sort_order', 'is_active'];
protected $casts = ['is_active' => 'boolean']; protected $casts = ['is_active' => 'boolean'];
public function getActivitylogOptions(): LogOptions public function getActivitylogOptions(): LogOptions
@ -103,6 +105,25 @@ class Category extends Model
->get(['id', 'name']); ->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 public static function themePills(int $limit = 8): Collection
{ {
return static::query() return static::query()

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Modules\Category\Providers; namespace Modules\Category\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -9,7 +10,7 @@ class CategoryServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
$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')); $this->loadRoutesFrom(module_path($this->moduleName, 'routes/web.php'));
$this->loadViewsFrom(module_path($this->moduleName, 'resources/views'), 'category'); $this->loadViewsFrom(module_path($this->moduleName, 'resources/views'), 'category');
} }

View File

@ -9,7 +9,7 @@ class ConversationServiceProvider extends ServiceProvider
{ {
public function boot(): void 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->loadRoutesFrom(module_path('Conversation', 'routes/web.php'));
$this->loadViewsFrom(module_path('Conversation', 'resources/views'), 'conversation'); $this->loadViewsFrom(module_path('Conversation', 'resources/views'), 'conversation');
@ -18,7 +18,5 @@ class ConversationServiceProvider extends ServiceProvider
}); });
} }
public function register(): void public function register(): void {}
{
}
} }

View File

@ -25,7 +25,7 @@ class DemoServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
$this->guardConfiguration(); $this->guardConfiguration();
$this->loadMigrationsFrom(module_path('Demo', 'database/migrations')); $this->loadMigrationsFrom(module_path('Demo', 'Database/migrations'));
$this->loadRoutesFrom(module_path('Demo', 'routes/web.php')); $this->loadRoutesFrom(module_path('Demo', 'routes/web.php'));
} }

View File

@ -8,12 +8,10 @@ class FavoriteServiceProvider extends ServiceProvider
{ {
public function boot(): void 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->loadRoutesFrom(module_path('Favorite', 'routes/web.php'));
$this->loadViewsFrom(module_path('Favorite', 'resources/views'), 'favorite'); $this->loadViewsFrom(module_path('Favorite', 'resources/views'), 'favorite');
} }
public function register(): void public function register(): void {}
{
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Modules\Listing\Providers; namespace Modules\Listing\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -6,12 +7,13 @@ use Illuminate\Support\ServiceProvider;
class ListingServiceProvider extends ServiceProvider class ListingServiceProvider extends ServiceProvider
{ {
protected string $moduleName = 'Listing'; protected string $moduleName = 'Listing';
protected string $moduleNameLower = 'listing'; protected string $moduleNameLower = 'listing';
public function boot(): void public function boot(): void
{ {
$this->loadViewsFrom(module_path($this->moduleName, 'resources/views'), $this->moduleNameLower); $this->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')); $this->loadRoutesFrom(module_path($this->moduleName, 'routes/web.php'));
} }

View File

@ -1,6 +1,8 @@
<?php <?php
namespace Modules\Location\Models; namespace Modules\Location\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity; use Spatie\Activitylog\Traits\LogsActivity;
@ -10,8 +12,14 @@ class City extends Model
use LogsActivity; use LogsActivity;
protected $fillable = ['name', 'country_id', 'is_active']; protected $fillable = ['name', 'country_id', 'is_active'];
protected $casts = ['is_active' => 'boolean']; protected $casts = ['is_active' => 'boolean'];
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
public function getActivitylogOptions(): LogOptions public function getActivitylogOptions(): LogOptions
{ {
return LogOptions::defaults() return LogOptions::defaults()
@ -20,6 +28,29 @@ class City extends Model
->dontSubmitEmptyLogs(); ->dontSubmitEmptyLogs();
} }
public function country() { return $this->belongsTo(Country::class); } public function country()
public function districts() { return $this->hasMany(District::class); } {
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();
}
} }

View File

@ -1,6 +1,8 @@
<?php <?php
namespace Modules\Location\Models; namespace Modules\Location\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity; use Spatie\Activitylog\Traits\LogsActivity;
@ -10,8 +12,14 @@ class Country extends Model
use LogsActivity; use LogsActivity;
protected $fillable = ['name', 'code', 'phone_code', 'flag', 'is_active']; protected $fillable = ['name', 'code', 'phone_code', 'flag', 'is_active'];
protected $casts = ['is_active' => 'boolean']; protected $casts = ['is_active' => 'boolean'];
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
public function getActivitylogOptions(): LogOptions public function getActivitylogOptions(): LogOptions
{ {
return LogOptions::defaults() return LogOptions::defaults()
@ -24,4 +32,31 @@ class Country extends Model
{ {
return $this->hasMany(City::class); 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();
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
namespace Modules\Location\Providers; namespace Modules\Location\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -9,7 +10,7 @@ class LocationServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
$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')); $this->loadRoutesFrom(module_path($this->moduleName, 'routes/web.php'));
} }

View File

@ -8,12 +8,10 @@ class UserServiceProvider extends ServiceProvider
{ {
public function boot(): void 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->loadRoutesFrom(module_path('User', 'routes/web.php'));
$this->loadViewsFrom(module_path('User', 'resources/views'), 'user'); $this->loadViewsFrom(module_path('User', 'resources/views'), 'user');
} }
public function register(): void public function register(): void {}
{
}
} }

View File

@ -9,7 +9,7 @@ class VideoServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
$this->loadViewsFrom(module_path('Video', 'resources/views'), 'video'); $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 public function register(): void

300
README.md
View File

@ -1,128 +1,76 @@
# OpenClassify # 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 - Laravel 12
- 🗂️ **Categories** — Hierarchical categories with icons - FilamentPHP v5
- 📍 **Locations** — Country and city management - `nwidart/laravel-modules`
- 👤 **User Profiles** — Manage your listings and account - Blade + Tailwind + Vite
- 🔐 **Admin Panel** — Full control via FilamentPHP v5 at `/admin` - Spatie Permission
- 🧭 **Frontend Panel** — Authenticated users manage listings, profile, videos, favorites, and inbox at `/panel` - Laravel Reverb + Echo (realtime chat)
- 🧪 **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
## Modules
## Tech Stack All business features live in `Modules/*` (routes, services, models, resources, views, seeders).
| Layer | Technology | Create a new module:
|-------|-----------|
| 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)
```bash ```bash
# Clone the repository php artisan module:make ModuleName
git clone https://github.com/openclassify/openclassify.git ```
cd openclassify
# Copy environment file Enable it in `modules_statuses.json`.
## Quick Start
### Docker
```bash
cp .env.example .env cp .env.example .env
# Start with Docker Compose (production-like)
docker compose up -d docker compose up -d
# The application will be available at http://localhost:8000
``` ```
### Default Accounts App URLs:
| Role | Email | Password | - Frontend: `http://localhost:8000`
|------|-------|----------| - Admin: `http://localhost:8000/admin`
| Admin | a@a.com | 236330 | - Panel: `http://localhost:8000/panel`
| Member | b@b.com | 36330 |
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 Requirements: PHP 8.2+, Composer, Node 18+, database server.
**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
```bash ```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 composer install
npm install npm install
# Setup environment
cp .env.example .env cp .env.example .env
php artisan key:generate php artisan key:generate
# Database (SQLite for quick start)
touch database/database.sqlite
php artisan migrate php artisan migrate
php artisan db:seed php artisan db:seed
# Start all services (server + queue + vite)
composer run dev composer run dev
``` ```
## Seeded Accounts
| Role | Email | Password |
|------|-------|----------|
| Admin | `a@a.com` | `236330` |
| Member | `b@b.com` | `36330` |
## Demo Mode ## 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` - `DB_CONNECTION=pgsql`
- `DEMO=1` - `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. Minimal `.env`:
### 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
```env ```env
DB_CONNECTION=pgsql
DEMO=1 DEMO=1
DEMO_TTL_MINUTES=360 DEMO_TTL_MINUTES=360
DEMO_SCHEMA_PREFIX=demo_ DEMO_SCHEMA_PREFIX=demo_
@ -131,195 +79,71 @@ DEMO_LOGIN_EMAIL=a@a.com
DEMO_PUBLIC_SCHEMA=public DEMO_PUBLIC_SCHEMA=public
``` ```
### Commands Commands:
```bash ```bash
php artisan migrate --force
php artisan db:seed --force
php artisan demo:prepare php artisan demo:prepare
php artisan demo:cleanup php artisan demo:cleanup
``` ```
### Panels Notes:
| Panel | URL | Access | - First guest homepage shows only `Prepare Demo`.
|-------|-----|--------| - `Prepare Demo` creates/reuses a private schema and logs in seeded admin.
| Admin | `/admin` | Users with `admin` role | - Expired demos are cleaned up automatically (hourly schedule).
| Frontend Panel | `/panel` | All authenticated users |
### Roles (Spatie Permission) ## Realtime Chat (Reverb)
| Role | Access | Set `.env`:
|------|--------|
| `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`:
```env ```env
BROADCAST_CONNECTION=reverb BROADCAST_CONNECTION=reverb
REVERB_APP_ID=app_id
REVERB_APP_ID=480227 REVERB_APP_KEY=app_key
REVERB_APP_KEY=your_key REVERB_APP_SECRET=app_secret
REVERB_APP_SECRET=your_secret
REVERB_HOST=localhost REVERB_HOST=localhost
REVERB_PORT=8080 REVERB_PORT=8080
REVERB_SCHEME=http REVERB_SCHEME=http
REVERB_SERVER_HOST=0.0.0.0 REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080 REVERB_SERVER_PORT=8080
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}" VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}" VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}" VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}" VITE_REVERB_SCHEME="${REVERB_SCHEME}"
``` ```
### 2. Start Services Start:
Use one command:
```bash ```bash
composer run dev composer run dev
``` ```
Or run separately: Channel strategy:
```bash - private channel: `users.{id}.inbox`
php artisan serve - events: `InboxMessageCreated`, `ConversationReadUpdated`
php artisan reverb:start --host=0.0.0.0 --port=8080
php artisan queue:listen --tries=1 --timeout=0
npm run dev
```
### 3. How It Works in This Codebase ## Test and Build
- 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
<p align="center">
<a href="https://openclassify.com">
<img src="https://raw.githubusercontent.com/openclassify/openclassify/master/public/openclassify-logo.png" width="220" alt="OpenClassify Logo">
</a>
</p>
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.
<p align="center">
<a href="https://github.com/openclassify/openclassify/graphs/contributors">
<img src="https://contrib.rocks/image?repo=openclassify/openclassify" alt="OpenClassify Contributors">
</a>
</p>
## 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
```bash ```bash
php artisan test php artisan test
php artisan optimize:clear
php artisan view:cache
``` ```
--- ## Production Checklist
## 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
```bash ```bash
php artisan migrate --force 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 config:cache
php artisan route:cache php artisan route:cache
php artisan view: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)

View File

@ -146,10 +146,10 @@ return [
// config/ // config/
'config' => ['path' => 'config', 'generate' => true], 'config' => ['path' => 'config', 'generate' => true],
// database/ // Database/
'factory' => ['path' => 'database/factories', 'generate' => true], 'factory' => ['path' => 'Database/Factories', 'generate' => true],
'migration' => ['path' => 'database/migrations', 'generate' => true], 'migration' => ['path' => 'Database/migrations', 'generate' => true],
'seeder' => ['path' => 'database/seeders', 'generate' => true], 'seeder' => ['path' => 'Database/Seeders', 'generate' => true],
// lang/ // lang/
'lang' => ['path' => 'lang', 'generate' => false], 'lang' => ['path' => 'lang', 'generate' => false],

View File

@ -6,25 +6,20 @@ use Illuminate\Support\Facades\Schema;
return new class extends Migration return new class extends Migration
{ {
public function up() public function up(): void
{ {
Schema::create('breezy_sessions', function (Blueprint $table) { Schema::create('breezy_sessions', function (Blueprint $table) {
$table->id(); $table->id();
$table->morphs('authenticatable'); $table->morphs('authenticatable');
$table->string('panel_id')->nullable(); $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_secret')->nullable();
$table->text('two_factor_recovery_codes')->nullable(); $table->text('two_factor_recovery_codes')->nullable();
$table->timestamp('two_factor_confirmed_at')->nullable(); $table->timestamp('two_factor_confirmed_at')->nullable();
$table->timestamps(); $table->timestamps();
}); });
} }
public function down() public function down(): void
{ {
Schema::dropIfExists('breezy_sessions'); Schema::dropIfExists('breezy_sessions');
} }

View File

@ -1,31 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('breezy_sessions', function (Blueprint $table) {
$table->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();
});
});
}
};

View File

@ -1,27 +1,31 @@
<?php <?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateActivityLogTable extends Migration return new class extends Migration
{ {
public function up() public function up(): void
{ {
Schema::connection(config('activitylog.database_connection'))->create(config('activitylog.table_name'), function (Blueprint $table) { Schema::connection(config('activitylog.database_connection'))
->create(config('activitylog.table_name'), function (Blueprint $table): void {
$table->bigIncrements('id'); $table->bigIncrements('id');
$table->string('log_name')->nullable(); $table->string('log_name')->nullable();
$table->text('description'); $table->text('description');
$table->nullableMorphs('subject', 'subject'); $table->nullableMorphs('subject', 'subject');
$table->string('event')->nullable();
$table->nullableMorphs('causer', 'causer'); $table->nullableMorphs('causer', 'causer');
$table->json('properties')->nullable(); $table->json('properties')->nullable();
$table->uuid('batch_uuid')->nullable();
$table->timestamps(); $table->timestamps();
$table->index('log_name'); $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'));
} }
};

View File

@ -1,22 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEventColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->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');
});
}
}

View File

@ -1,22 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBatchUuidColumnToActivityLogTable extends Migration
{
public function up()
{
Schema::connection(config('activitylog.database_connection'))->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');
});
}
}