mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
Refactor modules to SOLID structure
This commit is contained in:
parent
d2345cbeda
commit
6b3a8b8581
@ -2,23 +2,23 @@
|
|||||||
|
|
||||||
namespace Modules\Admin\Filament\Pages;
|
namespace Modules\Admin\Filament\Pages;
|
||||||
|
|
||||||
use App\Settings\GeneralSettings;
|
|
||||||
use App\Support\CountryCodeManager;
|
|
||||||
use App\Support\HomeSlideDefaults;
|
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
use Filament\Forms\Components\Hidden;
|
use Filament\Forms\Components\Hidden;
|
||||||
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\Pages\SettingsPage;
|
use Filament\Pages\SettingsPage;
|
||||||
use Filament\Schemas\Components\Utilities\Get;
|
use Filament\Schemas\Components\Utilities\Get;
|
||||||
use Filament\Schemas\Components\Utilities\Set;
|
use Filament\Schemas\Components\Utilities\Set;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Modules\Admin\Support\HomeSlideFormSchema;
|
use Modules\Admin\Support\HomeSlideFormSchema;
|
||||||
|
use Modules\Location\Support\CountryCodeManager;
|
||||||
use Modules\S3\Support\MediaStorage;
|
use Modules\S3\Support\MediaStorage;
|
||||||
|
use Modules\Site\App\Settings\GeneralSettings;
|
||||||
|
use Modules\Site\App\Support\HomeSlideDefaults;
|
||||||
use Tapp\FilamentCountryCodeField\Forms\Components\CountryCodeSelect;
|
use Tapp\FilamentCountryCodeField\Forms\Components\CountryCodeSelect;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
|
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
|
||||||
@ -31,9 +31,9 @@ class ManageGeneralSettings extends SettingsPage
|
|||||||
|
|
||||||
protected static ?string $navigationLabel = 'Genel Ayarlar';
|
protected static ?string $navigationLabel = 'Genel Ayarlar';
|
||||||
|
|
||||||
protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-cog-6-tooth';
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cog-6-tooth';
|
||||||
|
|
||||||
protected static string | UnitEnum | null $navigationGroup = 'Ayarlar';
|
protected static string|UnitEnum|null $navigationGroup = 'Ayarlar';
|
||||||
|
|
||||||
protected static ?int $navigationSort = 1;
|
protected static ?int $navigationSort = 1;
|
||||||
|
|
||||||
@ -246,7 +246,7 @@ class ManageGeneralSettings extends SettingsPage
|
|||||||
'home_slides' => $this->defaultHomeSlides(),
|
'home_slides' => $this->defaultHomeSlides(),
|
||||||
'site_logo_disk' => null,
|
'site_logo_disk' => null,
|
||||||
'sender_name' => $siteName,
|
'sender_name' => $siteName,
|
||||||
'sender_email' => (string) config('mail.from.address', 'info@' . $siteHost),
|
'sender_email' => (string) config('mail.from.address', 'info@'.$siteHost),
|
||||||
'default_language' => in_array(config('app.locale'), array_keys($this->localeOptions()), true) ? (string) config('app.locale') : 'en',
|
'default_language' => in_array(config('app.locale'), array_keys($this->localeOptions()), true) ? (string) config('app.locale') : 'en',
|
||||||
'default_country_code' => CountryCodeManager::normalizeCountryCode(config('app.default_country_code', '+90')),
|
'default_country_code' => CountryCodeManager::normalizeCountryCode(config('app.default_country_code', '+90')),
|
||||||
'currencies' => $this->normalizeCurrencies(config('app.currencies', ['TRY'])),
|
'currencies' => $this->normalizeCurrencies(config('app.currencies', ['TRY'])),
|
||||||
@ -272,7 +272,7 @@ class ManageGeneralSettings extends SettingsPage
|
|||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeCurrencies(null | array | string $state): array
|
private function normalizeCurrencies(null|array|string $state): array
|
||||||
{
|
{
|
||||||
$source = is_array($state) ? $state : (filled($state) ? [$state] : []);
|
$source = is_array($state) ? $state : (filled($state) ? [$state] : []);
|
||||||
|
|
||||||
|
|||||||
@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Admin\Filament\Pages;
|
|
||||||
|
|
||||||
use App\Settings\GeneralSettings;
|
|
||||||
use App\Support\HomeSlideDefaults;
|
|
||||||
use BackedEnum;
|
|
||||||
use Filament\Pages\SettingsPage;
|
|
||||||
use Filament\Schemas\Schema;
|
|
||||||
use Filament\Support\Enums\Width;
|
|
||||||
use Modules\Admin\Support\HomeSlideFormSchema;
|
|
||||||
use Modules\S3\Support\MediaStorage;
|
|
||||||
use UnitEnum;
|
|
||||||
|
|
||||||
class ManageHomeSlides extends SettingsPage
|
|
||||||
{
|
|
||||||
protected static string $settings = GeneralSettings::class;
|
|
||||||
|
|
||||||
protected static ?string $title = 'Home Slides';
|
|
||||||
|
|
||||||
protected static ?string $navigationLabel = 'Home Slides';
|
|
||||||
|
|
||||||
protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-photo';
|
|
||||||
|
|
||||||
protected static string | UnitEnum | null $navigationGroup = 'Content';
|
|
||||||
|
|
||||||
protected static ?int $navigationSort = 2;
|
|
||||||
|
|
||||||
protected Width | string | null $maxContentWidth = Width::Full;
|
|
||||||
|
|
||||||
protected function mutateFormDataBeforeFill(array $data): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'home_slides' => $this->normalizeHomeSlides(
|
|
||||||
$data['home_slides'] ?? $this->defaultHomeSlides(),
|
|
||||||
MediaStorage::storedDisk('public'),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function mutateFormDataBeforeSave(array $data): array
|
|
||||||
{
|
|
||||||
$data['home_slides'] = $this->normalizeHomeSlides($data['home_slides'] ?? [], MediaStorage::activeDisk());
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function form(Schema $schema): Schema
|
|
||||||
{
|
|
||||||
return $schema
|
|
||||||
->components([
|
|
||||||
HomeSlideFormSchema::make(
|
|
||||||
$this->defaultHomeSlides(),
|
|
||||||
fn ($state): array => $this->normalizeHomeSlides($state, MediaStorage::activeDisk()),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function defaultHomeSlides(): array
|
|
||||||
{
|
|
||||||
return HomeSlideDefaults::defaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function normalizeHomeSlides(mixed $state, ?string $defaultDisk = null): array
|
|
||||||
{
|
|
||||||
return HomeSlideDefaults::normalize($state, $defaultDisk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,42 +2,14 @@
|
|||||||
|
|
||||||
namespace Modules\Admin\Filament\Resources;
|
namespace Modules\Admin\Filament\Resources;
|
||||||
|
|
||||||
use A909M\FilamentStateFusion\Forms\Components\StateFusionSelect;
|
|
||||||
use A909M\FilamentStateFusion\Tables\Columns\StateFusionSelectColumn;
|
|
||||||
use A909M\FilamentStateFusion\Tables\Filters\StateFusionSelectFilter;
|
|
||||||
use App\Support\CountryCodeManager;
|
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Cheesegrits\FilamentGoogleMaps\Fields\Map;
|
|
||||||
use Filament\Forms\Components\DatePicker;
|
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
|
||||||
use Filament\Forms\Components\Textarea;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Forms\Components\Toggle;
|
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Components\Section;
|
|
||||||
use Filament\Schemas\Components\Utilities\Get;
|
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Tables\Columns\IconColumn;
|
|
||||||
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Enums\FiltersLayout;
|
|
||||||
use Filament\Tables\Filters\Filter;
|
|
||||||
use Filament\Tables\Filters\SelectFilter;
|
|
||||||
use Filament\Tables\Filters\TernaryFilter;
|
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
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\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
use Modules\Listing\Support\Filament\AdminListingResourceSchema;
|
||||||
use Modules\Listing\Support\ListingPanelHelper;
|
|
||||||
use Modules\Location\Models\City;
|
|
||||||
use Modules\Location\Models\Country;
|
|
||||||
use Modules\Video\Support\Filament\VideoFormSchema;
|
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
|
|
||||||
|
|
||||||
class ListingResource extends Resource
|
class ListingResource extends Resource
|
||||||
{
|
{
|
||||||
@ -49,138 +21,12 @@ class ListingResource extends Resource
|
|||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema->schema([
|
return $schema->schema(AdminListingResourceSchema::form());
|
||||||
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')
|
|
||||||
->numeric()
|
|
||||||
->currencyMask(thousandSeparator: ',', decimalSeparator: '.', precision: 2),
|
|
||||||
Select::make('currency')
|
|
||||||
->options(fn () => ListingPanelHelper::currencyOptions())
|
|
||||||
->default(fn () => ListingPanelHelper::defaultCurrency())
|
|
||||||
->required(),
|
|
||||||
Select::make('category_id')
|
|
||||||
->label('Category')
|
|
||||||
->options(fn (): array => Category::activeIdNameOptions())
|
|
||||||
->searchable()
|
|
||||||
->live()
|
|
||||||
->afterStateUpdated(fn ($state, $set) => $set('custom_fields', []))
|
|
||||||
->nullable(),
|
|
||||||
Select::make('user_id')->relationship('user', 'email')->label('Owner')->searchable()->preload()->nullable(),
|
|
||||||
Section::make('Custom Fields')
|
|
||||||
->description('Category specific listing attributes.')
|
|
||||||
->schema(fn (Get $get): array => ListingCustomFieldSchemaBuilder::formComponents(
|
|
||||||
($categoryId = $get('category_id')) ? (int) $categoryId : null
|
|
||||||
))
|
|
||||||
->columns(2)
|
|
||||||
->columnSpanFull()
|
|
||||||
->visible(fn (Get $get): bool => ListingCustomFieldSchemaBuilder::hasFields(
|
|
||||||
($categoryId = $get('category_id')) ? (int) $categoryId : null
|
|
||||||
)),
|
|
||||||
StateFusionSelect::make('status')->required(),
|
|
||||||
PhoneInput::make('contact_phone')->defaultCountry(CountryCodeManager::defaultCountryIso2())->nullable(),
|
|
||||||
TextInput::make('contact_email')->email()->maxLength(255),
|
|
||||||
Toggle::make('is_featured')->default(false),
|
|
||||||
Select::make('country')
|
|
||||||
->label('Country')
|
|
||||||
->options(fn (): array => Country::nameOptions())
|
|
||||||
->searchable()
|
|
||||||
->preload()
|
|
||||||
->live()
|
|
||||||
->afterStateUpdated(fn ($state, $set) => $set('city', null))
|
|
||||||
->nullable(),
|
|
||||||
Select::make('city')
|
|
||||||
->label('City')
|
|
||||||
->options(fn (Get $get): array => City::nameOptions($get('country')))
|
|
||||||
->searchable()
|
|
||||||
->preload()
|
|
||||||
->nullable(),
|
|
||||||
Map::make('location')
|
|
||||||
->label('Location')
|
|
||||||
->visible(fn (): bool => ListingPanelHelper::googleMapsEnabled())
|
|
||||||
->draggable()
|
|
||||||
->clickable()
|
|
||||||
->autocomplete('city')
|
|
||||||
->autocompleteReverse(true)
|
|
||||||
->reverseGeocode([
|
|
||||||
'city' => '%L',
|
|
||||||
])
|
|
||||||
->defaultLocation([41.0082, 28.9784])
|
|
||||||
->defaultZoom(10)
|
|
||||||
->height('320px')
|
|
||||||
->columnSpanFull(),
|
|
||||||
SpatieMediaLibraryFileUpload::make('images')
|
|
||||||
->collection('listing-images')
|
|
||||||
->multiple()
|
|
||||||
->image()
|
|
||||||
->reorderable(),
|
|
||||||
VideoFormSchema::listingSection(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
public static function table(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table->columns([
|
return AdminListingResourceSchema::configureTable($table, static::class);
|
||||||
SpatieMediaLibraryImageColumn::make('images')
|
|
||||||
->collection('listing-images')
|
|
||||||
->circular(),
|
|
||||||
TextColumn::make('id')->sortable(),
|
|
||||||
TextColumn::make('title')->searchable()->sortable()->limit(40),
|
|
||||||
TextColumn::make('category.name')->label('Category')->sortable(),
|
|
||||||
TextColumn::make('user.email')->label('Owner')->searchable()->toggleable()->sortable(),
|
|
||||||
TextColumn::make('price')
|
|
||||||
->currency(fn (Listing $record): string => $record->currency ?: ListingPanelHelper::defaultCurrency())
|
|
||||||
->sortable(),
|
|
||||||
StateFusionSelectColumn::make('status')->sortable(),
|
|
||||||
IconColumn::make('is_featured')->boolean()->label('Featured')->sortable(),
|
|
||||||
TextColumn::make('city')->sortable(),
|
|
||||||
TextColumn::make('country')->sortable(),
|
|
||||||
TextColumn::make('created_at')->dateTime()->sortable(),
|
|
||||||
])->filters([
|
|
||||||
StateFusionSelectFilter::make('status'),
|
|
||||||
SelectFilter::make('category_id')
|
|
||||||
->label('Category')
|
|
||||||
->relationship('category', 'name')
|
|
||||||
->searchable()
|
|
||||||
->preload(),
|
|
||||||
SelectFilter::make('user_id')
|
|
||||||
->label('Owner')
|
|
||||||
->relationship('user', 'email')
|
|
||||||
->searchable()
|
|
||||||
->preload(),
|
|
||||||
SelectFilter::make('country')
|
|
||||||
->options(fn (): array => Country::nameOptions())
|
|
||||||
->searchable(),
|
|
||||||
SelectFilter::make('city')
|
|
||||||
->options(fn (): array => City::nameOptions(null, false))
|
|
||||||
->searchable(),
|
|
||||||
TernaryFilter::make('is_featured')->label('Featured'),
|
|
||||||
Filter::make('created_at')
|
|
||||||
->label('Created Date')
|
|
||||||
->schema([
|
|
||||||
DatePicker::make('from')->label('From'),
|
|
||||||
DatePicker::make('until')->label('Until'),
|
|
||||||
])
|
|
||||||
->query(fn (Builder $query, array $data): Builder => $query
|
|
||||||
->when($data['from'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('created_at', '>=', $date))
|
|
||||||
->when($data['until'] ?? null, fn (Builder $query, string $date): Builder => $query->whereDate('created_at', '<=', $date))),
|
|
||||||
Filter::make('price')
|
|
||||||
->label('Price Range')
|
|
||||||
->schema([
|
|
||||||
TextInput::make('min')->numeric()->label('Min'),
|
|
||||||
TextInput::make('max')->numeric()->label('Max'),
|
|
||||||
])
|
|
||||||
->query(fn (Builder $query, array $data): Builder => $query
|
|
||||||
->when($data['min'] ?? null, fn (Builder $query, string $amount): Builder => $query->where('price', '>=', (float) $amount))
|
|
||||||
->when($data['max'] ?? null, fn (Builder $query, string $amount): Builder => $query->where('price', '<=', (float) $amount))),
|
|
||||||
])
|
|
||||||
->filtersLayout(FiltersLayout::AboveContent)
|
|
||||||
->filtersFormColumns(3)
|
|
||||||
->filtersFormWidth('7xl')
|
|
||||||
->persistFiltersInSession()
|
|
||||||
->defaultSort('id', 'desc')
|
|
||||||
->actions(ResourceTableActions::editActivityDelete(static::class));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Admin\Filament\Widgets;
|
namespace Modules\Admin\Filament\Widgets;
|
||||||
|
|
||||||
use Filament\Widgets\ChartWidget;
|
use Filament\Widgets\ChartWidget;
|
||||||
@ -8,6 +9,8 @@ class ListingsTrendChart extends ChartWidget
|
|||||||
{
|
{
|
||||||
protected static ?int $sort = 2;
|
protected static ?int $sort = 2;
|
||||||
|
|
||||||
|
protected int|string|array $columnSpan = 'full';
|
||||||
|
|
||||||
protected ?string $heading = 'Listing Creation Trend';
|
protected ?string $heading = 'Listing Creation Trend';
|
||||||
|
|
||||||
protected ?string $description = 'Daily listing volume by selected period.';
|
protected ?string $description = 'Daily listing volume by selected period.';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Admin\Providers;
|
namespace Modules\Admin\Providers;
|
||||||
|
|
||||||
use App\Http\Middleware\BootstrapAppData;
|
|
||||||
use A909M\FilamentStateFusion\FilamentStateFusionPlugin;
|
use A909M\FilamentStateFusion\FilamentStateFusionPlugin;
|
||||||
use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin;
|
use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin;
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
@ -21,13 +21,10 @@ use Illuminate\Routing\Middleware\SubstituteBindings;
|
|||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||||
use Jeffgreco13\FilamentBreezy\BreezyCore;
|
use Jeffgreco13\FilamentBreezy\BreezyCore;
|
||||||
use MWGuerra\FileManager\FileManagerPlugin;
|
|
||||||
use MWGuerra\FileManager\Filament\Pages\FileManager;
|
|
||||||
use Modules\Demo\App\Http\Middleware\ResolveDemoRequest;
|
use Modules\Demo\App\Http\Middleware\ResolveDemoRequest;
|
||||||
use Modules\Admin\Filament\Resources\CategoryResource;
|
use Modules\Site\App\Http\Middleware\BootstrapAppData;
|
||||||
use Modules\Admin\Filament\Resources\ListingResource;
|
use MWGuerra\FileManager\Filament\Pages\FileManager;
|
||||||
use Modules\Admin\Filament\Resources\LocationResource;
|
use MWGuerra\FileManager\FileManagerPlugin;
|
||||||
use Modules\Admin\Filament\Resources\UserResource;
|
|
||||||
|
|
||||||
class AdminPanelProvider extends PanelProvider
|
class AdminPanelProvider extends PanelProvider
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@ -114,6 +114,72 @@ class Category extends Model
|
|||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function activeCount(): int
|
||||||
|
{
|
||||||
|
return (int) static::query()
|
||||||
|
->active()
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function homeParentCategories(int $limit = 8): Collection
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->whereNull('parent_id')
|
||||||
|
->ordered()
|
||||||
|
->limit($limit)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function headerNavigationItems(int $limit = 8): array
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->whereNull('parent_id')
|
||||||
|
->ordered()
|
||||||
|
->limit($limit)
|
||||||
|
->get(['id', 'name', 'icon'])
|
||||||
|
->map(fn (self $category): array => [
|
||||||
|
'id' => (int) $category->id,
|
||||||
|
'name' => (string) $category->name,
|
||||||
|
'icon_url' => $category->iconUrl(),
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function activeAiCatalog(): Collection
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->ordered()
|
||||||
|
->get(['id', 'name', 'parent_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function panelQuickCatalog(): array
|
||||||
|
{
|
||||||
|
$all = static::query()
|
||||||
|
->active()
|
||||||
|
->ordered()
|
||||||
|
->get(['id', 'name', 'parent_id', 'icon']);
|
||||||
|
|
||||||
|
$childrenCount = static::query()
|
||||||
|
->active()
|
||||||
|
->selectRaw('parent_id, count(*) as aggregate')
|
||||||
|
->whereNotNull('parent_id')
|
||||||
|
->groupBy('parent_id')
|
||||||
|
->pluck('aggregate', 'parent_id');
|
||||||
|
|
||||||
|
return $all
|
||||||
|
->map(fn (self $category): array => [
|
||||||
|
'id' => (int) $category->id,
|
||||||
|
'name' => (string) $category->name,
|
||||||
|
'parent_id' => $category->parent_id ? (int) $category->parent_id : null,
|
||||||
|
'icon' => $category->icon,
|
||||||
|
'has_children' => ((int) ($childrenCount[$category->id] ?? 0)) > 0,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
public static function rootIdNameOptions(): array
|
public static function rootIdNameOptions(): array
|
||||||
{
|
{
|
||||||
return static::query()
|
return static::query()
|
||||||
|
|||||||
@ -107,7 +107,7 @@ class ConversationDemoSeeder extends Seeder
|
|||||||
$readAfterMinutes = $payload['read_after_minutes'];
|
$readAfterMinutes = $payload['read_after_minutes'];
|
||||||
$readAt = is_numeric($readAfterMinutes) ? $createdAt->copy()->addMinutes((int) $readAfterMinutes) : null;
|
$readAt = is_numeric($readAfterMinutes) ? $createdAt->copy()->addMinutes((int) $readAfterMinutes) : null;
|
||||||
|
|
||||||
$message = new ConversationMessage();
|
$message = new ConversationMessage;
|
||||||
$message->forceFill([
|
$message->forceFill([
|
||||||
'conversation_id' => $conversation->getKey(),
|
'conversation_id' => $conversation->getKey(),
|
||||||
'sender_id' => $sender->getKey(),
|
'sender_id' => $sender->getKey(),
|
||||||
@ -5,10 +5,10 @@
|
|||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
||||||
@include('panel.partials.sidebar', ['activeMenu' => 'inbox'])
|
@include('panel::partials.sidebar', ['activeMenu' => 'inbox'])
|
||||||
|
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
@include('panel.partials.page-header', [
|
@include('panel::partials.page-header', [
|
||||||
'title' => 'Inbox',
|
'title' => 'Inbox',
|
||||||
'description' => 'Read and reply to buyer messages from the same panel shell used across the site.',
|
'description' => 'Read and reply to buyer messages from the same panel shell used across the site.',
|
||||||
'actions' => $requiresLogin ?? false
|
'actions' => $requiresLogin ?? false
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
namespace Modules\Demo\App\Support;
|
namespace Modules\Demo\App\Support;
|
||||||
|
|
||||||
use App\Settings\GeneralSettings;
|
|
||||||
use Illuminate\Contracts\Foundation\Application;
|
use Illuminate\Contracts\Foundation\Application;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Modules\Demo\App\Models\DemoInstance;
|
use Modules\Demo\App\Models\DemoInstance;
|
||||||
|
use Modules\Site\App\Settings\GeneralSettings;
|
||||||
use Modules\User\App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
use Spatie\Permission\PermissionRegistrar;
|
use Spatie\Permission\PermissionRegistrar;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
||||||
@include('panel.partials.sidebar', ['activeMenu' => 'favorites', 'activeFavoritesTab' => $activeTab])
|
@include('panel::partials.sidebar', ['activeMenu' => 'favorites', 'activeFavoritesTab' => $activeTab])
|
||||||
|
|
||||||
<section class="bg-white border border-slate-200">
|
<section class="bg-white border border-slate-200">
|
||||||
@if($requiresLogin ?? false)
|
@if($requiresLogin ?? false)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@ -30,10 +31,26 @@ return new class extends Migration
|
|||||||
$table->decimal('longitude', 10, 7)->nullable();
|
$table->decimal('longitude', 10, 7)->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Schema::create('listing_custom_fields', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name')->unique();
|
||||||
|
$table->string('label');
|
||||||
|
$table->string('type', 32);
|
||||||
|
$table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
|
||||||
|
$table->text('placeholder')->nullable();
|
||||||
|
$table->text('help_text')->nullable();
|
||||||
|
$table->json('options')->nullable();
|
||||||
|
$table->boolean('is_required')->default(false);
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->unsignedInteger('sort_order')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
|
Schema::dropIfExists('listing_custom_fields');
|
||||||
Schema::dropIfExists('listings');
|
Schema::dropIfExists('listings');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -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::create('listing_custom_fields', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name')->unique();
|
|
||||||
$table->string('label');
|
|
||||||
$table->string('type', 32);
|
|
||||||
$table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
|
|
||||||
$table->text('placeholder')->nullable();
|
|
||||||
$table->text('help_text')->nullable();
|
|
||||||
$table->json('options')->nullable();
|
|
||||||
$table->boolean('is_required')->default(false);
|
|
||||||
$table->boolean('is_active')->default(true);
|
|
||||||
$table->unsignedInteger('sort_order')->default(0);
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('listing_custom_fields');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,22 +1,25 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Listing\Models;
|
namespace Modules\Listing\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
use Modules\Listing\Support\ListingImageViewData;
|
|
||||||
use Modules\Listing\States\ListingStatus;
|
use Modules\Listing\States\ListingStatus;
|
||||||
|
use Modules\Listing\Support\ListingImageViewData;
|
||||||
use Modules\Listing\Support\ListingPanelHelper;
|
use Modules\Listing\Support\ListingPanelHelper;
|
||||||
|
use Modules\User\App\Models\User;
|
||||||
|
use Modules\Video\Enums\VideoStatus;
|
||||||
use Modules\Video\Models\Video;
|
use Modules\Video\Models\Video;
|
||||||
use Spatie\Image\Enums\Fit;
|
|
||||||
use Spatie\Activitylog\LogOptions;
|
use Spatie\Activitylog\LogOptions;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
use Spatie\Image\Enums\Fit;
|
||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||||
@ -97,7 +100,7 @@ class Listing extends Model implements HasMedia
|
|||||||
return $query->where('status', 'active');
|
return $query->where('status', 'active');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopeOwnedByUser(Builder $query, int | string | null $userId): Builder
|
public function scopeOwnedByUser(Builder $query, int|string|null $userId): Builder
|
||||||
{
|
{
|
||||||
return $query->where('user_id', $userId);
|
return $query->where('user_id', $userId);
|
||||||
}
|
}
|
||||||
@ -127,6 +130,24 @@ class Listing extends Model implements HasMedia
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function scopeWithPanelIndexState(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query
|
||||||
|
->with('category:id,name')
|
||||||
|
->withCount('favoritedByUsers')
|
||||||
|
->withCount('videos')
|
||||||
|
->withCount([
|
||||||
|
'videos as ready_videos_count' => fn (Builder $videoQuery): Builder => $videoQuery
|
||||||
|
->whereNotNull('path')
|
||||||
|
->where('is_active', true),
|
||||||
|
'videos as pending_videos_count' => fn (Builder $videoQuery): Builder => $videoQuery
|
||||||
|
->whereIn('status', [
|
||||||
|
VideoStatus::Pending->value,
|
||||||
|
VideoStatus::Processing->value,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function scopeForCategory(Builder $query, ?int $categoryId): Builder
|
public function scopeForCategory(Builder $query, ?int $categoryId): Builder
|
||||||
{
|
{
|
||||||
return $query->forCategoryIds(Category::listingFilterIds($categoryId));
|
return $query->forCategoryIds(Category::listingFilterIds($categoryId));
|
||||||
@ -272,7 +293,7 @@ class Listing extends Model implements HasMedia
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function panelStatusCountsForUser(int | string $userId): array
|
public static function panelStatusCountsForUser(int|string $userId): array
|
||||||
{
|
{
|
||||||
$counts = static::query()
|
$counts = static::query()
|
||||||
->ownedByUser($userId)
|
->ownedByUser($userId)
|
||||||
@ -289,6 +310,49 @@ class Listing extends Model implements HasMedia
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function activeCount(): int
|
||||||
|
{
|
||||||
|
return (int) static::query()
|
||||||
|
->active()
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function homeFeatured(int $limit = 4): Collection
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->where('is_featured', true)
|
||||||
|
->latest()
|
||||||
|
->take($limit)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function homeRecent(int $limit = 8): Collection
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->latest()
|
||||||
|
->take($limit)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function panelIndexDataForUser(User $user, string $search, string $status): array
|
||||||
|
{
|
||||||
|
$listings = static::query()
|
||||||
|
->ownedByUser($user->getKey())
|
||||||
|
->withPanelIndexState()
|
||||||
|
->searchTerm($search)
|
||||||
|
->forPanelStatus($status)
|
||||||
|
->latest('id')
|
||||||
|
->paginate(10)
|
||||||
|
->withQueryString();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'listings' => $listings,
|
||||||
|
'counts' => static::panelStatusCountsForUser($user->getKey()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function panelPrimaryImageUrl(): ?string
|
public function panelPrimaryImageUrl(): ?string
|
||||||
{
|
{
|
||||||
return $this->primaryImageUrl('card', 'desktop');
|
return $this->primaryImageUrl('card', 'desktop');
|
||||||
@ -435,6 +499,34 @@ class Listing extends Model implements HasMedia
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function loadPanelEditor(): self
|
||||||
|
{
|
||||||
|
return $this->load([
|
||||||
|
'category:id,name',
|
||||||
|
'videos:id,listing_id,title,status,is_active,path,upload_path,duration_seconds,size',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assertOwnedBy(User $user): void
|
||||||
|
{
|
||||||
|
abort_unless((int) $this->user_id === (int) $user->getKey(), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAsSold(): void
|
||||||
|
{
|
||||||
|
$this->forceFill([
|
||||||
|
'status' => 'sold',
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function republish(): void
|
||||||
|
{
|
||||||
|
$this->forceFill([
|
||||||
|
'status' => 'active',
|
||||||
|
'expires_at' => now()->addDays(self::DEFAULT_PANEL_EXPIRY_WINDOW_DAYS),
|
||||||
|
])->save();
|
||||||
|
}
|
||||||
|
|
||||||
public function updateFromPanel(array $attributes): void
|
public function updateFromPanel(array $attributes): void
|
||||||
{
|
{
|
||||||
$payload = Arr::only($attributes, [
|
$payload = Arr::only($attributes, [
|
||||||
@ -460,7 +552,7 @@ class Listing extends Model implements HasMedia
|
|||||||
$this->forceFill($payload)->save();
|
$this->forceFill($payload)->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function createFromFrontend(array $data, null | int | string $userId): self
|
public static function createFromFrontend(array $data, null|int|string $userId): self
|
||||||
{
|
{
|
||||||
$baseSlug = Str::slug((string) ($data['title'] ?? 'listing'));
|
$baseSlug = Str::slug((string) ($data['title'] ?? 'listing'));
|
||||||
$baseSlug = $baseSlug !== '' ? $baseSlug : 'listing';
|
$baseSlug = $baseSlug !== '' ? $baseSlug : 'listing';
|
||||||
|
|||||||
@ -9,10 +9,15 @@ use Modules\Category\Models\Category;
|
|||||||
class ListingCustomField extends Model
|
class ListingCustomField extends Model
|
||||||
{
|
{
|
||||||
public const TYPE_TEXT = 'text';
|
public const TYPE_TEXT = 'text';
|
||||||
|
|
||||||
public const TYPE_TEXTAREA = 'textarea';
|
public const TYPE_TEXTAREA = 'textarea';
|
||||||
|
|
||||||
public const TYPE_NUMBER = 'number';
|
public const TYPE_NUMBER = 'number';
|
||||||
|
|
||||||
public const TYPE_SELECT = 'select';
|
public const TYPE_SELECT = 'select';
|
||||||
|
|
||||||
public const TYPE_BOOLEAN = 'boolean';
|
public const TYPE_BOOLEAN = 'boolean';
|
||||||
|
|
||||||
public const TYPE_DATE = 'date';
|
public const TYPE_DATE = 'date';
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
@ -100,4 +105,26 @@ class ListingCustomField extends Model
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function panelFieldDefinitions(?int $categoryId): array
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->forCategory($categoryId)
|
||||||
|
->ordered()
|
||||||
|
->get(['name', 'label', 'type', 'is_required', 'placeholder', 'help_text', 'options'])
|
||||||
|
->map(fn (self $field): array => [
|
||||||
|
'name' => (string) $field->name,
|
||||||
|
'label' => (string) $field->label,
|
||||||
|
'type' => (string) $field->type,
|
||||||
|
'is_required' => (bool) $field->is_required,
|
||||||
|
'placeholder' => $field->placeholder,
|
||||||
|
'help_text' => $field->help_text,
|
||||||
|
'options' => collect($field->options ?? [])
|
||||||
|
->map(fn ($option): string => (string) $option)
|
||||||
|
->values()
|
||||||
|
->all(),
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
176
Modules/Listing/Support/Filament/AdminListingResourceSchema.php
Normal file
176
Modules/Listing/Support/Filament/AdminListingResourceSchema.php
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Listing\Support\Filament;
|
||||||
|
|
||||||
|
use A909M\FilamentStateFusion\Forms\Components\StateFusionSelect;
|
||||||
|
use A909M\FilamentStateFusion\Tables\Columns\StateFusionSelectColumn;
|
||||||
|
use A909M\FilamentStateFusion\Tables\Filters\StateFusionSelectFilter;
|
||||||
|
use Cheesegrits\FilamentGoogleMaps\Fields\Map;
|
||||||
|
use Filament\Forms\Components\DatePicker;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\Utilities\Get;
|
||||||
|
use Filament\Tables\Columns\IconColumn;
|
||||||
|
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Enums\FiltersLayout;
|
||||||
|
use Filament\Tables\Filters\Filter;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Filament\Tables\Filters\TernaryFilter;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Modules\Admin\Support\Filament\ResourceTableActions;
|
||||||
|
use Modules\Category\Models\Category;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
||||||
|
use Modules\Listing\Support\ListingPanelHelper;
|
||||||
|
use Modules\Location\Models\City;
|
||||||
|
use Modules\Location\Models\Country;
|
||||||
|
use Modules\Location\Support\CountryCodeManager;
|
||||||
|
use Modules\Video\Support\Filament\VideoFormSchema;
|
||||||
|
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
|
||||||
|
|
||||||
|
class AdminListingResourceSchema
|
||||||
|
{
|
||||||
|
public static function form(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
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')
|
||||||
|
->numeric()
|
||||||
|
->currencyMask(thousandSeparator: ',', decimalSeparator: '.', precision: 2),
|
||||||
|
Select::make('currency')
|
||||||
|
->options(fn (): array => ListingPanelHelper::currencyOptions())
|
||||||
|
->default(fn (): string => ListingPanelHelper::defaultCurrency())
|
||||||
|
->required(),
|
||||||
|
Select::make('category_id')
|
||||||
|
->label('Category')
|
||||||
|
->options(fn (): array => Category::activeIdNameOptions())
|
||||||
|
->searchable()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn ($state, $set) => $set('custom_fields', []))
|
||||||
|
->nullable(),
|
||||||
|
Select::make('user_id')->relationship('user', 'email')->label('Owner')->searchable()->preload()->nullable(),
|
||||||
|
Section::make('Custom Fields')
|
||||||
|
->description('Category specific listing attributes.')
|
||||||
|
->schema(fn (Get $get): array => ListingCustomFieldSchemaBuilder::formComponents(
|
||||||
|
($categoryId = $get('category_id')) ? (int) $categoryId : null
|
||||||
|
))
|
||||||
|
->columns(2)
|
||||||
|
->columnSpanFull()
|
||||||
|
->visible(fn (Get $get): bool => ListingCustomFieldSchemaBuilder::hasFields(
|
||||||
|
($categoryId = $get('category_id')) ? (int) $categoryId : null
|
||||||
|
)),
|
||||||
|
StateFusionSelect::make('status')->required(),
|
||||||
|
PhoneInput::make('contact_phone')->defaultCountry(CountryCodeManager::defaultCountryIso2())->nullable(),
|
||||||
|
TextInput::make('contact_email')->email()->maxLength(255),
|
||||||
|
Toggle::make('is_featured')->default(false),
|
||||||
|
Select::make('country')
|
||||||
|
->label('Country')
|
||||||
|
->options(fn (): array => Country::nameOptions())
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn ($state, $set) => $set('city', null))
|
||||||
|
->nullable(),
|
||||||
|
Select::make('city')
|
||||||
|
->label('City')
|
||||||
|
->options(fn (Get $get): array => City::nameOptions($get('country')))
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->nullable(),
|
||||||
|
Map::make('location')
|
||||||
|
->label('Location')
|
||||||
|
->visible(fn (): bool => ListingPanelHelper::googleMapsEnabled())
|
||||||
|
->draggable()
|
||||||
|
->clickable()
|
||||||
|
->autocomplete('city')
|
||||||
|
->autocompleteReverse(true)
|
||||||
|
->reverseGeocode([
|
||||||
|
'city' => '%L',
|
||||||
|
])
|
||||||
|
->defaultLocation([41.0082, 28.9784])
|
||||||
|
->defaultZoom(10)
|
||||||
|
->height('320px')
|
||||||
|
->columnSpanFull(),
|
||||||
|
SpatieMediaLibraryFileUpload::make('images')
|
||||||
|
->collection('listing-images')
|
||||||
|
->multiple()
|
||||||
|
->image()
|
||||||
|
->reorderable(),
|
||||||
|
VideoFormSchema::listingSection(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function configureTable(Table $table, string $resourceClass): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
SpatieMediaLibraryImageColumn::make('images')
|
||||||
|
->collection('listing-images')
|
||||||
|
->circular(),
|
||||||
|
TextColumn::make('id')->sortable(),
|
||||||
|
TextColumn::make('title')->searchable()->sortable()->limit(40),
|
||||||
|
TextColumn::make('category.name')->label('Category')->sortable(),
|
||||||
|
TextColumn::make('user.email')->label('Owner')->searchable()->toggleable()->sortable(),
|
||||||
|
TextColumn::make('price')
|
||||||
|
->currency(fn (Listing $record): string => $record->currency ?: ListingPanelHelper::defaultCurrency())
|
||||||
|
->sortable(),
|
||||||
|
StateFusionSelectColumn::make('status')->sortable(),
|
||||||
|
IconColumn::make('is_featured')->boolean()->label('Featured')->sortable(),
|
||||||
|
TextColumn::make('city')->sortable(),
|
||||||
|
TextColumn::make('country')->sortable(),
|
||||||
|
TextColumn::make('created_at')->dateTime()->sortable(),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
StateFusionSelectFilter::make('status'),
|
||||||
|
SelectFilter::make('category_id')
|
||||||
|
->label('Category')
|
||||||
|
->relationship('category', 'name')
|
||||||
|
->searchable()
|
||||||
|
->preload(),
|
||||||
|
SelectFilter::make('user_id')
|
||||||
|
->label('Owner')
|
||||||
|
->relationship('user', 'email')
|
||||||
|
->searchable()
|
||||||
|
->preload(),
|
||||||
|
SelectFilter::make('country')
|
||||||
|
->options(fn (): array => Country::nameOptions())
|
||||||
|
->searchable(),
|
||||||
|
SelectFilter::make('city')
|
||||||
|
->options(fn (): array => City::nameOptions(null, false))
|
||||||
|
->searchable(),
|
||||||
|
TernaryFilter::make('is_featured')->label('Featured'),
|
||||||
|
Filter::make('created_at')
|
||||||
|
->label('Created Date')
|
||||||
|
->schema([
|
||||||
|
DatePicker::make('from')->label('From'),
|
||||||
|
DatePicker::make('until')->label('Until'),
|
||||||
|
])
|
||||||
|
->query(fn (Builder $query, array $data): Builder => $query
|
||||||
|
->when($data['from'] ?? null, fn (Builder $builder, string $date): Builder => $builder->whereDate('created_at', '>=', $date))
|
||||||
|
->when($data['until'] ?? null, fn (Builder $builder, string $date): Builder => $builder->whereDate('created_at', '<=', $date))),
|
||||||
|
Filter::make('price')
|
||||||
|
->label('Price Range')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('min')->numeric()->label('Min'),
|
||||||
|
TextInput::make('max')->numeric()->label('Max'),
|
||||||
|
])
|
||||||
|
->query(fn (Builder $query, array $data): Builder => $query
|
||||||
|
->when($data['min'] ?? null, fn (Builder $builder, string $amount): Builder => $builder->where('price', '>=', (float) $amount))
|
||||||
|
->when($data['max'] ?? null, fn (Builder $builder, string $amount): Builder => $builder->where('price', '<=', (float) $amount))),
|
||||||
|
])
|
||||||
|
->filtersLayout(FiltersLayout::AboveContentCollapsible)
|
||||||
|
->filtersFormColumns(3)
|
||||||
|
->filtersFormWidth('7xl')
|
||||||
|
->persistFiltersInSession()
|
||||||
|
->defaultSort('id', 'desc')
|
||||||
|
->actions(ResourceTableActions::editActivityDelete($resourceClass));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,8 +4,8 @@ namespace Modules\Listing\Support;
|
|||||||
|
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
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\Schemas\Components\Component;
|
use Filament\Schemas\Components\Component;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@ -22,9 +22,6 @@ class ListingCustomFieldSchemaBuilder
|
|||||||
->exists();
|
->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, Component>
|
|
||||||
*/
|
|
||||||
public static function formComponents(?int $categoryId): array
|
public static function formComponents(?int $categoryId): array
|
||||||
{
|
{
|
||||||
return ListingCustomField::query()
|
return ListingCustomField::query()
|
||||||
@ -38,10 +35,6 @@ class ListingCustomFieldSchemaBuilder
|
|||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $values
|
|
||||||
* @return array<int, array{label: string, value: string}>
|
|
||||||
*/
|
|
||||||
public static function presentableValues(?int $categoryId, array $values): array
|
public static function presentableValues(?int $categoryId, array $values): array
|
||||||
{
|
{
|
||||||
if ($values === []) {
|
if ($values === []) {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Listing\Support;
|
namespace Modules\Listing\Support;
|
||||||
|
|
||||||
use App\Settings\GeneralSettings;
|
use Modules\Site\App\Settings\GeneralSettings;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class ListingPanelHelper
|
class ListingPanelHelper
|
||||||
@ -32,7 +32,7 @@ class ListingPanelHelper
|
|||||||
return self::currencyCodes()[0] ?? 'USD';
|
return self::currencyCodes()[0] ?? 'USD';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function normalizeCurrency(null | string $currency): string
|
public static function normalizeCurrency(?string $currency): string
|
||||||
{
|
{
|
||||||
$normalized = strtoupper(substr(trim((string) $currency), 0, 3));
|
$normalized = strtoupper(substr(trim((string) $currency), 0, 3));
|
||||||
$codes = self::currencyCodes();
|
$codes = self::currencyCodes();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Support;
|
namespace Modules\Listing\Support;
|
||||||
|
|
||||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
@ -12,16 +12,6 @@ use function Laravel\Ai\agent;
|
|||||||
|
|
||||||
class QuickListingCategorySuggester
|
class QuickListingCategorySuggester
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @return array{
|
|
||||||
* detected: bool,
|
|
||||||
* category_id: int|null,
|
|
||||||
* confidence: float|null,
|
|
||||||
* reason: string,
|
|
||||||
* alternatives: array<int>,
|
|
||||||
* error: string|null
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public function suggestFromImage(UploadedFile $image): array
|
public function suggestFromImage(UploadedFile $image): array
|
||||||
{
|
{
|
||||||
$provider = (string) config('quick-listing.ai_provider', 'openai');
|
$provider = (string) config('quick-listing.ai_provider', 'openai');
|
||||||
@ -39,11 +29,7 @@ class QuickListingCategorySuggester
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$categories = Category::query()
|
$categories = Category::activeAiCatalog();
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('sort_order')
|
|
||||||
->orderBy('name')
|
|
||||||
->get(['id', 'name', 'parent_id']);
|
|
||||||
|
|
||||||
if ($categories->isEmpty()) {
|
if ($categories->isEmpty()) {
|
||||||
return [
|
return [
|
||||||
@ -131,10 +117,6 @@ class QuickListingCategorySuggester
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Collection<int, Category> $categories
|
|
||||||
* @return Collection<int, array{id: int, path: string}>
|
|
||||||
*/
|
|
||||||
private function buildCatalog(Collection $categories): Collection
|
private function buildCatalog(Collection $categories): Collection
|
||||||
{
|
{
|
||||||
$byId = $categories->keyBy('id');
|
$byId = $categories->keyBy('id');
|
||||||
@ -156,4 +138,3 @@ class QuickListingCategorySuggester
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,12 +229,12 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="listing-filter-card px-4 py-3 hidden lg:flex flex-col xl:flex-row xl:items-center gap-3">
|
<div class="listing-results-bar listing-filter-card hidden lg:flex">
|
||||||
<p class="text-sm text-slate-700 mr-auto">
|
<p class="listing-results-meta">
|
||||||
<strong>{{ number_format($resultListingsCount) }}</strong>
|
<strong>{{ number_format($resultListingsCount) }}</strong>
|
||||||
{{ $activeCategoryName !== '' ? ' listings found in '.$activeCategoryName : ' listings found' }}
|
{{ $activeCategoryName !== '' ? ' listings found in '.$activeCategoryName : ' listings found' }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="listing-results-actions">
|
||||||
@auth
|
@auth
|
||||||
<form method="POST" action="{{ route('favorites.searches.store') }}">
|
<form method="POST" action="{{ route('favorites.searches.store') }}">
|
||||||
@csrf
|
@csrf
|
||||||
@ -276,9 +276,9 @@
|
|||||||
<input type="hidden" name="date_filter" value="{{ $dateFilter }}">
|
<input type="hidden" name="date_filter" value="{{ $dateFilter }}">
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<label class="h-10 px-4 rounded-full border border-slate-300 bg-white inline-flex items-center gap-2 text-sm font-semibold text-slate-700">
|
<label class="listing-results-sort">
|
||||||
<span>Sort by</span>
|
<span>Sort by</span>
|
||||||
<select name="sort" class="bg-transparent text-sm font-semibold focus:outline-none" onchange="this.form.submit()">
|
<select name="sort" class="listing-results-sort-select" onchange="this.form.submit()">
|
||||||
<option value="smart" @selected($sort === 'smart')>Recommended</option>
|
<option value="smart" @selected($sort === 'smart')>Recommended</option>
|
||||||
<option value="newest" @selected($sort === 'newest')>Newest</option>
|
<option value="newest" @selected($sort === 'newest')>Newest</option>
|
||||||
<option value="oldest" @selected($sort === 'oldest')>Oldest</option>
|
<option value="oldest" @selected($sort === 'oldest')>Oldest</option>
|
||||||
@ -570,7 +570,6 @@
|
|||||||
countrySelect.value = matchedCountryOption.value;
|
countrySelect.value = matchedCountryOption.value;
|
||||||
await loadCities(matchedCountryOption.value, cityName);
|
await loadCities(matchedCountryOption.value, cityName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// no-op
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -43,9 +43,6 @@ class LocationSeeder extends Seeder
|
|||||||
->delete();
|
->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, array{code: string, name: string, phone_code: string}>
|
|
||||||
*/
|
|
||||||
private function countries(): array
|
private function countries(): array
|
||||||
{
|
{
|
||||||
$countries = [];
|
$countries = [];
|
||||||
@ -84,7 +81,7 @@ class LocationSeeder extends Seeder
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$key = 'filament-country-code-field::countries.' . $value;
|
$key = 'filament-country-code-field::countries.'.$value;
|
||||||
$labelEn = trim((string) trans($key, [], 'en'));
|
$labelEn = trim((string) trans($key, [], 'en'));
|
||||||
|
|
||||||
$name = $labelEn !== '' && $labelEn !== $key ? $labelEn : strtoupper($value);
|
$name = $labelEn !== '' && $labelEn !== $key ? $labelEn : strtoupper($value);
|
||||||
@ -112,9 +109,6 @@ class LocationSeeder extends Seeder
|
|||||||
return substr($normalized, 0, 10);
|
return substr($normalized, 0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
private function turkeyCities(): array
|
private function turkeyCities(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Location\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Modules\Location\Models\City;
|
||||||
|
use Modules\Location\Models\Country;
|
||||||
|
|
||||||
|
class LocationLookupController extends Controller
|
||||||
|
{
|
||||||
|
public function cities(string $country): JsonResponse
|
||||||
|
{
|
||||||
|
$countryModel = Country::resolveLookup($country);
|
||||||
|
|
||||||
|
if (! $countryModel) {
|
||||||
|
return response()->json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($countryModel->cityPayloads());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function districts(City $city): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json($city->districtPayloads());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,8 @@ namespace Modules\Location\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Spatie\Activitylog\LogOptions;
|
use Spatie\Activitylog\LogOptions;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
|
||||||
@ -28,12 +30,12 @@ class City extends Model
|
|||||||
->dontSubmitEmptyLogs();
|
->dontSubmitEmptyLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function country()
|
public function country(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Country::class);
|
return $this->belongsTo(Country::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function districts()
|
public function districts(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(District::class);
|
return $this->hasMany(District::class);
|
||||||
}
|
}
|
||||||
@ -53,4 +55,31 @@ class City extends Model
|
|||||||
->pluck('name', 'name')
|
->pluck('name', 'name')
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function quickCreateOptions(): array
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'country_id'])
|
||||||
|
->map(fn (self $city): array => [
|
||||||
|
'id' => (int) $city->id,
|
||||||
|
'name' => (string) $city->name,
|
||||||
|
'country_id' => (int) $city->country_id,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function districtPayloads(): array
|
||||||
|
{
|
||||||
|
return $this->districts()
|
||||||
|
->orderBy('name')
|
||||||
|
->get()
|
||||||
|
->map(fn (District $district): array => [
|
||||||
|
'id' => (int) $district->id,
|
||||||
|
'name' => (string) $district->name,
|
||||||
|
'city_id' => (int) $district->city_id,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ namespace Modules\Location\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Spatie\Activitylog\LogOptions;
|
use Spatie\Activitylog\LogOptions;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ class Country extends Model
|
|||||||
->dontSubmitEmptyLogs();
|
->dontSubmitEmptyLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cities()
|
public function cities(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(City::class);
|
return $this->hasMany(City::class);
|
||||||
}
|
}
|
||||||
@ -59,4 +60,75 @@ class Country extends Model
|
|||||||
->pluck('name', 'name')
|
->pluck('name', 'name')
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function quickCreateOptions(): array
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name'])
|
||||||
|
->map(fn (self $country): array => [
|
||||||
|
'id' => (int) $country->id,
|
||||||
|
'name' => (string) $country->name,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function headerLocationOptions(): array
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->active()
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'code'])
|
||||||
|
->map(fn (self $country): array => [
|
||||||
|
'id' => (int) $country->id,
|
||||||
|
'name' => (string) $country->name,
|
||||||
|
'code' => strtoupper((string) $country->code),
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resolveLookup(string $value): ?self
|
||||||
|
{
|
||||||
|
$lookupValue = trim($value);
|
||||||
|
|
||||||
|
if ($lookupValue === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lookupCode = strtoupper($lookupValue);
|
||||||
|
$lookupName = mb_strtolower($lookupValue);
|
||||||
|
|
||||||
|
return static::query()
|
||||||
|
->where(function (Builder $query) use ($lookupCode, $lookupName, $lookupValue): void {
|
||||||
|
if (ctype_digit($lookupValue)) {
|
||||||
|
$query->orWhere('id', (int) $lookupValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query
|
||||||
|
->orWhereRaw('UPPER(code) = ?', [$lookupCode])
|
||||||
|
->orWhereRaw('LOWER(name) = ?', [$lookupName]);
|
||||||
|
})
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cityPayloads(bool $onlyActive = true): array
|
||||||
|
{
|
||||||
|
$cities = $this->cities()
|
||||||
|
->when($onlyActive, fn (Builder $query): Builder => $query->active())
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'country_id']);
|
||||||
|
|
||||||
|
if ($onlyActive && $cities->isEmpty()) {
|
||||||
|
return $this->cityPayloads(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cities
|
||||||
|
->map(fn (City $city): array => [
|
||||||
|
'id' => (int) $city->id,
|
||||||
|
'name' => (string) $city->name,
|
||||||
|
'country_id' => (int) $city->country_id,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Support;
|
namespace Modules\Location\Support;
|
||||||
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Tapp\FilamentCountryCodeField\Enums\CountriesEnum;
|
use Tapp\FilamentCountryCodeField\Enums\CountriesEnum;
|
||||||
@ -17,7 +17,7 @@ class CountryCodeManager
|
|||||||
return self::iso2FromCountryCode(self::defaultCountryCode()) ?? 'TR';
|
return self::iso2FromCountryCode(self::defaultCountryCode()) ?? 'TR';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function normalizeCountryCode(null | string $value): string
|
public static function normalizeCountryCode(?string $value): string
|
||||||
{
|
{
|
||||||
$value = trim((string) $value);
|
$value = trim((string) $value);
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ class CountryCodeManager
|
|||||||
return self::countryCodeFromIso2($value) ?? '+90';
|
return self::countryCodeFromIso2($value) ?? '+90';
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function isValidCountryCode(null | string $value): bool
|
public static function isValidCountryCode(?string $value): bool
|
||||||
{
|
{
|
||||||
if (! filled($value)) {
|
if (! filled($value)) {
|
||||||
return false;
|
return false;
|
||||||
@ -41,7 +41,7 @@ class CountryCodeManager
|
|||||||
return self::countries()->contains(fn (array $country): bool => $country['country_code'] === trim((string) $value));
|
return self::countries()->contains(fn (array $country): bool => $country['country_code'] === trim((string) $value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function countryCodeFromIso2(null | string $iso2): ?string
|
public static function countryCodeFromIso2(?string $iso2): ?string
|
||||||
{
|
{
|
||||||
$iso2 = strtoupper(trim((string) $iso2));
|
$iso2 = strtoupper(trim((string) $iso2));
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class CountryCodeManager
|
|||||||
->first(fn (array $country): bool => $country['iso2'] === $iso2)['country_code'] ?? null;
|
->first(fn (array $country): bool => $country['iso2'] === $iso2)['country_code'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function iso2FromCountryCode(null | string $countryCode): ?string
|
public static function iso2FromCountryCode(?string $countryCode): ?string
|
||||||
{
|
{
|
||||||
$countryCode = trim((string) $countryCode);
|
$countryCode = trim((string) $countryCode);
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ class CountryCodeManager
|
|||||||
->first(fn (array $country): bool => $country['country_code'] === $countryCode)['iso2'] ?? null;
|
->first(fn (array $country): bool => $country['country_code'] === $countryCode)['iso2'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function labelFromCountryCode(null | string $countryCode): ?string
|
public static function labelFromCountryCode(?string $countryCode): ?string
|
||||||
{
|
{
|
||||||
$countryCode = trim((string) $countryCode);
|
$countryCode = trim((string) $countryCode);
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ class CountryCodeManager
|
|||||||
->first(fn (array $country): bool => $country['country_code'] === $countryCode)['english_label'] ?? null;
|
->first(fn (array $country): bool => $country['country_code'] === $countryCode)['english_label'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function countryCodeFromLabelOrCode(null | string $value): ?string
|
public static function countryCodeFromLabelOrCode(?string $value): ?string
|
||||||
{
|
{
|
||||||
$value = trim((string) $value);
|
$value = trim((string) $value);
|
||||||
|
|
||||||
@ -111,7 +111,7 @@ class CountryCodeManager
|
|||||||
})['country_code'] ?? null;
|
})['country_code'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function normalizeStoredCountry(null | string $value): ?string
|
public static function normalizeStoredCountry(?string $value): ?string
|
||||||
{
|
{
|
||||||
$value = trim((string) $value);
|
$value = trim((string) $value);
|
||||||
|
|
||||||
@ -128,9 +128,6 @@ class CountryCodeManager
|
|||||||
return self::labelFromCountryCode($countryCode) ?? $value;
|
return self::labelFromCountryCode($countryCode) ?? $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Collection<int, array{country_code: string, iso2: string, label: string, english_label: string}>
|
|
||||||
*/
|
|
||||||
private static function countries(): Collection
|
private static function countries(): Collection
|
||||||
{
|
{
|
||||||
static $countries;
|
static $countries;
|
||||||
@ -1,50 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Modules\Location\Models\Country;
|
use Modules\Location\Http\Controllers\LocationLookupController;
|
||||||
|
|
||||||
Route::get('/locations/cities/{country}', function (string $country) {
|
Route::middleware('web')->group(function () {
|
||||||
$lookupValue = trim($country);
|
Route::get('/locations/cities/{country}', [LocationLookupController::class, 'cities'])
|
||||||
|
->name('locations.cities');
|
||||||
if ($lookupValue === '') {
|
Route::get('/locations/districts/{city}', [LocationLookupController::class, 'districts'])
|
||||||
return response()->json([]);
|
->name('locations.districts');
|
||||||
}
|
});
|
||||||
|
|
||||||
$lookupCode = strtoupper($lookupValue);
|
|
||||||
$lookupName = mb_strtolower($lookupValue);
|
|
||||||
|
|
||||||
$countryModel = Country::query()
|
|
||||||
->where(function ($query) use ($lookupValue, $lookupCode, $lookupName): void {
|
|
||||||
if (ctype_digit($lookupValue)) {
|
|
||||||
$query->orWhere('id', (int) $lookupValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
$query
|
|
||||||
->orWhereRaw('UPPER(code) = ?', [$lookupCode])
|
|
||||||
->orWhereRaw('LOWER(name) = ?', [$lookupName]);
|
|
||||||
})
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $countryModel) {
|
|
||||||
return response()->json([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$activeCities = $countryModel->cities()
|
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('name')
|
|
||||||
->get(['id', 'name', 'country_id']);
|
|
||||||
|
|
||||||
if ($activeCities->isNotEmpty()) {
|
|
||||||
return response()->json($activeCities);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json(
|
|
||||||
$countryModel->cities()
|
|
||||||
->orderBy('name')
|
|
||||||
->get(['id', 'name', 'country_id'])
|
|
||||||
);
|
|
||||||
})->name('locations.cities');
|
|
||||||
|
|
||||||
Route::get('/locations/districts/{city}', function (\Modules\Location\Models\City $city) {
|
|
||||||
return response()->json($city->districts);
|
|
||||||
})->name('locations.districts');
|
|
||||||
|
|||||||
171
Modules/Panel/App/Http/Controllers/PanelController.php
Normal file
171
Modules/Panel/App/Http/Controllers/PanelController.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Panel\App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
||||||
|
use Modules\Listing\Support\ListingPanelHelper;
|
||||||
|
use Modules\Panel\App\Http\Requests\StoreVideoRequest;
|
||||||
|
use Modules\Panel\App\Http\Requests\UpdateListingRequest;
|
||||||
|
use Modules\Panel\App\Http\Requests\UpdateVideoRequest;
|
||||||
|
use Modules\Video\Models\Video;
|
||||||
|
|
||||||
|
class PanelController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): RedirectResponse
|
||||||
|
{
|
||||||
|
return redirect()->route('panel.listings.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('panel::create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listings(Request $request): View
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$search = trim((string) $request->string('search'));
|
||||||
|
$status = (string) $request->string('status', 'all');
|
||||||
|
|
||||||
|
if (! in_array($status, ['all', 'sold', 'expired'], true)) {
|
||||||
|
$status = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = Listing::panelIndexDataForUser($user, $search, $status);
|
||||||
|
|
||||||
|
return view('panel::listings', [
|
||||||
|
'listings' => $payload['listings'],
|
||||||
|
'status' => $status,
|
||||||
|
'search' => $search,
|
||||||
|
'counts' => $payload['counts'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editListing(Request $request, Listing $listing): View
|
||||||
|
{
|
||||||
|
$listing->assertOwnedBy($request->user());
|
||||||
|
|
||||||
|
return view('panel::edit-listing', [
|
||||||
|
'listing' => $listing->loadPanelEditor(),
|
||||||
|
'customFieldValues' => ListingCustomFieldSchemaBuilder::presentableValues(
|
||||||
|
$listing->category_id ? (int) $listing->category_id : null,
|
||||||
|
(array) $listing->custom_fields,
|
||||||
|
),
|
||||||
|
'statusOptions' => Listing::panelStatusOptions(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateListing(UpdateListingRequest $request, Listing $listing): RedirectResponse
|
||||||
|
{
|
||||||
|
$listing->assertOwnedBy($request->user());
|
||||||
|
|
||||||
|
$listing->updateFromPanel($request->validated() + [
|
||||||
|
'currency' => $listing->currency ?: ListingPanelHelper::defaultCurrency(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('panel.listings.edit', $listing)
|
||||||
|
->with('success', 'Listing updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function videos(Request $request): View
|
||||||
|
{
|
||||||
|
return view('panel::videos', Video::panelIndexDataForUser($request->user()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeVideo(StoreVideoRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$listing = $request->user()->listings()->whereKey($validated['listing_id'])->firstOrFail();
|
||||||
|
|
||||||
|
$video = Video::createFromUploadedFile($listing, $request->file('video_file'), [
|
||||||
|
'title' => $validated['title'] ?? null,
|
||||||
|
'description' => $validated['description'] ?? null,
|
||||||
|
'sort_order' => Video::nextSortOrderForListing($listing),
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('panel.videos.edit', $video)
|
||||||
|
->with('success', 'Video uploaded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editVideo(Request $request, Video $video): View
|
||||||
|
{
|
||||||
|
$video->assertOwnedBy($request->user());
|
||||||
|
|
||||||
|
return view('panel::video-edit', [
|
||||||
|
'video' => $video->load('listing:id,title,user_id'),
|
||||||
|
'listingOptions' => $request->user()->panelListingOptions(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateVideo(UpdateVideoRequest $request, Video $video): RedirectResponse
|
||||||
|
{
|
||||||
|
$video->assertOwnedBy($request->user());
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$listing = $request->user()->listings()->whereKey($validated['listing_id'])->firstOrFail();
|
||||||
|
|
||||||
|
$video->updateFromPanel([
|
||||||
|
'listing_id' => $listing->getKey(),
|
||||||
|
'title' => $validated['title'] ?? null,
|
||||||
|
'description' => $validated['description'] ?? null,
|
||||||
|
'video_file' => $request->file('video_file'),
|
||||||
|
'is_active' => $request->boolean('is_active'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('panel.videos.edit', $video)
|
||||||
|
->with('success', 'Video updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyVideo(Request $request, Video $video): RedirectResponse
|
||||||
|
{
|
||||||
|
$video->assertOwnedBy($request->user());
|
||||||
|
$video->delete();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('panel.videos.index')
|
||||||
|
->with('success', 'Video deleted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function profile(Request $request): View
|
||||||
|
{
|
||||||
|
$user = $request->user()->loadPanelProfile();
|
||||||
|
|
||||||
|
return view('panel::profile', [
|
||||||
|
'user' => $user,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyListing(Request $request, Listing $listing): RedirectResponse
|
||||||
|
{
|
||||||
|
$listing->assertOwnedBy($request->user());
|
||||||
|
$listing->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Listing removed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markListingAsSold(Request $request, Listing $listing): RedirectResponse
|
||||||
|
{
|
||||||
|
$listing->assertOwnedBy($request->user());
|
||||||
|
$listing->markAsSold();
|
||||||
|
|
||||||
|
return back()->with('success', 'Listing marked as sold.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function republishListing(Request $request, Listing $listing): RedirectResponse
|
||||||
|
{
|
||||||
|
$listing->assertOwnedBy($request->user());
|
||||||
|
$listing->republish();
|
||||||
|
|
||||||
|
return back()->with('success', 'Listing republished.');
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Modules/Panel/App/Http/Requests/StoreVideoRequest.php
Normal file
33
Modules/Panel/App/Http/Requests/StoreVideoRequest.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Panel\App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreVideoRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'listing_id' => ['required', 'integer', 'exists:listings,id'],
|
||||||
|
'title' => ['nullable', 'string', 'max:255'],
|
||||||
|
'description' => ['nullable', 'string', 'max:2000'],
|
||||||
|
'video_file' => ['required', 'file', 'mimes:mp4,mov,webm,m4v', 'max:256000'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'listing_id.required' => 'Choose a listing for the video.',
|
||||||
|
'listing_id.exists' => 'Choose a valid listing for the video.',
|
||||||
|
'video_file.required' => 'A video file is required.',
|
||||||
|
'video_file.mimes' => 'Video must be an mp4, mov, webm, or m4v file.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
42
Modules/Panel/App/Http/Requests/UpdateListingRequest.php
Normal file
42
Modules/Panel/App/Http/Requests/UpdateListingRequest.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Panel\App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
|
||||||
|
class UpdateListingRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => ['required', 'string', 'max:255'],
|
||||||
|
'description' => ['nullable', 'string', 'max:5000'],
|
||||||
|
'price' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'status' => ['required', Rule::in(array_keys(Listing::panelStatusOptions()))],
|
||||||
|
'contact_phone' => ['nullable', 'string', 'max:60'],
|
||||||
|
'contact_email' => ['nullable', 'email', 'max:255'],
|
||||||
|
'country' => ['nullable', 'string', 'max:255'],
|
||||||
|
'city' => ['nullable', 'string', 'max:255'],
|
||||||
|
'expires_at' => ['nullable', 'date'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title.required' => 'Listing title is required.',
|
||||||
|
'price.numeric' => 'Listing price must be numeric.',
|
||||||
|
'status.required' => 'Listing status is required.',
|
||||||
|
'status.in' => 'Listing status is invalid.',
|
||||||
|
'contact_email.email' => 'Contact email must be valid.',
|
||||||
|
'expires_at.date' => 'Expiry date must be a valid date.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Modules/Panel/App/Http/Requests/UpdateVideoRequest.php
Normal file
33
Modules/Panel/App/Http/Requests/UpdateVideoRequest.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Panel\App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateVideoRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'listing_id' => ['required', 'integer', 'exists:listings,id'],
|
||||||
|
'title' => ['nullable', 'string', 'max:255'],
|
||||||
|
'description' => ['nullable', 'string', 'max:2000'],
|
||||||
|
'video_file' => ['nullable', 'file', 'mimes:mp4,mov,webm,m4v', 'max:256000'],
|
||||||
|
'is_active' => ['nullable', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'listing_id.required' => 'Choose a listing for the video.',
|
||||||
|
'listing_id.exists' => 'Choose a valid listing for the video.',
|
||||||
|
'video_file.mimes' => 'Video must be an mp4, mov, webm, or m4v file.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Livewire;
|
namespace Modules\Panel\App\Livewire;
|
||||||
|
|
||||||
use App\Support\QuickListingCategorySuggester;
|
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -16,6 +15,7 @@ use Modules\Listing\Models\Listing;
|
|||||||
use Modules\Listing\Models\ListingCustomField;
|
use Modules\Listing\Models\ListingCustomField;
|
||||||
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
||||||
use Modules\Listing\Support\ListingPanelHelper;
|
use Modules\Listing\Support\ListingPanelHelper;
|
||||||
|
use Modules\Listing\Support\QuickListingCategorySuggester;
|
||||||
use Modules\Location\Models\City;
|
use Modules\Location\Models\City;
|
||||||
use Modules\Location\Models\Country;
|
use Modules\Location\Models\Country;
|
||||||
use Modules\S3\Support\MediaStorage;
|
use Modules\S3\Support\MediaStorage;
|
||||||
@ -28,36 +28,59 @@ class PanelQuickListingForm extends Component
|
|||||||
use WithFileUploads;
|
use WithFileUploads;
|
||||||
|
|
||||||
private const TOTAL_STEPS = 5;
|
private const TOTAL_STEPS = 5;
|
||||||
|
|
||||||
private const DRAFT_SESSION_KEY = 'panel_quick_listing_draft';
|
private const DRAFT_SESSION_KEY = 'panel_quick_listing_draft';
|
||||||
|
|
||||||
private const OTHER_CITY_ID = -1;
|
private const OTHER_CITY_ID = -1;
|
||||||
|
|
||||||
public array $photos = [];
|
public array $photos = [];
|
||||||
|
|
||||||
public array $videos = [];
|
public array $videos = [];
|
||||||
|
|
||||||
public array $categories = [];
|
public array $categories = [];
|
||||||
|
|
||||||
public array $countries = [];
|
public array $countries = [];
|
||||||
|
|
||||||
public array $cities = [];
|
public array $cities = [];
|
||||||
|
|
||||||
public array $listingCustomFields = [];
|
public array $listingCustomFields = [];
|
||||||
|
|
||||||
public array $customFieldValues = [];
|
public array $customFieldValues = [];
|
||||||
|
|
||||||
public int $currentStep = 1;
|
public int $currentStep = 1;
|
||||||
|
|
||||||
public string $categorySearch = '';
|
public string $categorySearch = '';
|
||||||
|
|
||||||
public ?int $selectedCategoryId = null;
|
public ?int $selectedCategoryId = null;
|
||||||
|
|
||||||
public ?int $activeParentCategoryId = null;
|
public ?int $activeParentCategoryId = null;
|
||||||
|
|
||||||
public ?int $detectedCategoryId = null;
|
public ?int $detectedCategoryId = null;
|
||||||
|
|
||||||
public ?float $detectedConfidence = null;
|
public ?float $detectedConfidence = null;
|
||||||
|
|
||||||
public ?string $detectedReason = null;
|
public ?string $detectedReason = null;
|
||||||
|
|
||||||
public ?string $detectedError = null;
|
public ?string $detectedError = null;
|
||||||
|
|
||||||
public array $detectedAlternatives = [];
|
public array $detectedAlternatives = [];
|
||||||
|
|
||||||
public bool $isDetecting = false;
|
public bool $isDetecting = false;
|
||||||
|
|
||||||
public string $listingTitle = '';
|
public string $listingTitle = '';
|
||||||
|
|
||||||
public string $price = '';
|
public string $price = '';
|
||||||
|
|
||||||
public string $description = '';
|
public string $description = '';
|
||||||
|
|
||||||
public ?int $selectedCountryId = null;
|
public ?int $selectedCountryId = null;
|
||||||
|
|
||||||
public ?int $selectedCityId = null;
|
public ?int $selectedCityId = null;
|
||||||
|
|
||||||
public bool $isPublishing = false;
|
public bool $isPublishing = false;
|
||||||
|
|
||||||
public bool $shouldPersistDraft = true;
|
public bool $shouldPersistDraft = true;
|
||||||
|
|
||||||
public ?string $publishError = null;
|
public ?string $publishError = null;
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
@ -70,7 +93,7 @@ class PanelQuickListingForm extends Component
|
|||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('panel.quick-create');
|
return view('panel::quick-create');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dehydrate(): void
|
public function dehydrate(): void
|
||||||
@ -596,10 +619,6 @@ class PanelQuickListingForm extends Component
|
|||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$profilePhone = Profile::query()
|
|
||||||
->where('user_id', $user->getKey())
|
|
||||||
->value('phone');
|
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'title' => trim($this->listingTitle),
|
'title' => trim($this->listingTitle),
|
||||||
'description' => trim($this->description),
|
'description' => trim($this->description),
|
||||||
@ -609,7 +628,7 @@ class PanelQuickListingForm extends Component
|
|||||||
'status' => 'pending',
|
'status' => 'pending',
|
||||||
'custom_fields' => $this->sanitizedCustomFieldValues(),
|
'custom_fields' => $this->sanitizedCustomFieldValues(),
|
||||||
'contact_email' => (string) $user->email,
|
'contact_email' => (string) $user->email,
|
||||||
'contact_phone' => $profilePhone,
|
'contact_phone' => Profile::phoneForUser($user),
|
||||||
'country' => $this->selectedCountryName,
|
'country' => $this->selectedCountryName,
|
||||||
'city' => $this->selectedCityName,
|
'city' => $this->selectedCityName,
|
||||||
];
|
];
|
||||||
@ -674,75 +693,18 @@ class PanelQuickListingForm extends Component
|
|||||||
|
|
||||||
private function loadCategories(): void
|
private function loadCategories(): void
|
||||||
{
|
{
|
||||||
$all = Category::query()
|
$this->categories = Category::panelQuickCatalog();
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('sort_order')
|
|
||||||
->orderBy('name')
|
|
||||||
->get(['id', 'name', 'parent_id', 'icon']);
|
|
||||||
|
|
||||||
$childrenCount = Category::query()
|
|
||||||
->where('is_active', true)
|
|
||||||
->selectRaw('parent_id, count(*) as aggregate')
|
|
||||||
->whereNotNull('parent_id')
|
|
||||||
->groupBy('parent_id')
|
|
||||||
->pluck('aggregate', 'parent_id');
|
|
||||||
|
|
||||||
$this->categories = $all
|
|
||||||
->map(fn (Category $category): array => [
|
|
||||||
'id' => (int) $category->id,
|
|
||||||
'name' => (string) $category->name,
|
|
||||||
'parent_id' => $category->parent_id ? (int) $category->parent_id : null,
|
|
||||||
'icon' => $category->icon,
|
|
||||||
'has_children' => ((int) ($childrenCount[$category->id] ?? 0)) > 0,
|
|
||||||
])
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadLocations(): void
|
private function loadLocations(): void
|
||||||
{
|
{
|
||||||
$this->countries = Country::query()
|
$this->countries = Country::quickCreateOptions();
|
||||||
->where('is_active', true)
|
$this->cities = City::quickCreateOptions();
|
||||||
->orderBy('name')
|
|
||||||
->get(['id', 'name'])
|
|
||||||
->map(fn (Country $country): array => [
|
|
||||||
'id' => (int) $country->id,
|
|
||||||
'name' => (string) $country->name,
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
|
|
||||||
$this->cities = City::query()
|
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('name')
|
|
||||||
->get(['id', 'name', 'country_id'])
|
|
||||||
->map(fn (City $city): array => [
|
|
||||||
'id' => (int) $city->id,
|
|
||||||
'name' => (string) $city->name,
|
|
||||||
'country_id' => (int) $city->country_id,
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadListingCustomFields(): void
|
private function loadListingCustomFields(): void
|
||||||
{
|
{
|
||||||
$this->listingCustomFields = ListingCustomField::query()
|
$this->listingCustomFields = ListingCustomField::panelFieldDefinitions($this->selectedCategoryId);
|
||||||
->active()
|
|
||||||
->forCategory($this->selectedCategoryId)
|
|
||||||
->ordered()
|
|
||||||
->get(['name', 'label', 'type', 'is_required', 'placeholder', 'help_text', 'options'])
|
|
||||||
->map(fn (ListingCustomField $field): array => [
|
|
||||||
'name' => (string) $field->name,
|
|
||||||
'label' => (string) $field->label,
|
|
||||||
'type' => (string) $field->type,
|
|
||||||
'is_required' => (bool) $field->is_required,
|
|
||||||
'placeholder' => $field->placeholder,
|
|
||||||
'help_text' => $field->help_text,
|
|
||||||
'options' => collect($field->options ?? [])
|
|
||||||
->map(fn ($option): string => (string) $option)
|
|
||||||
->values()
|
|
||||||
->all(),
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
|
|
||||||
$allowed = collect($this->listingCustomFields)->pluck('name')->all();
|
$allowed = collect($this->listingCustomFields)->pluck('name')->all();
|
||||||
$this->customFieldValues = collect($this->customFieldValues)->only($allowed)->all();
|
$this->customFieldValues = collect($this->customFieldValues)->only($allowed)->all();
|
||||||
@ -762,7 +724,7 @@ class PanelQuickListingForm extends Component
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$profile = Profile::query()->where('user_id', $user->getKey())->first();
|
$profile = Profile::detailsForUser($user);
|
||||||
|
|
||||||
if (! $profile) {
|
if (! $profile) {
|
||||||
return;
|
return;
|
||||||
18
Modules/Panel/App/Providers/PanelServiceProvider.php
Normal file
18
Modules/Panel/App/Providers/PanelServiceProvider.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Panel\App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Modules\Panel\App\Livewire\PanelQuickListingForm;
|
||||||
|
|
||||||
|
class PanelServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->loadRoutesFrom(module_path('Panel', 'routes/web.php'));
|
||||||
|
$this->loadViewsFrom(module_path('Panel', 'resources/views'), 'panel');
|
||||||
|
|
||||||
|
Livewire::component('panel-quick-listing-form', PanelQuickListingForm::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Modules/Panel/module.json
Normal file
11
Modules/Panel/module.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Panel",
|
||||||
|
"alias": "panel",
|
||||||
|
"description": "Authenticated seller panel",
|
||||||
|
"keywords": [],
|
||||||
|
"priority": 0,
|
||||||
|
"providers": [
|
||||||
|
"Modules\\Panel\\App\\Providers\\PanelServiceProvider"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@
|
|||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
||||||
@include('panel.partials.sidebar', ['activeMenu' => 'listings'])
|
@include('panel::partials.sidebar', ['activeMenu' => 'listings'])
|
||||||
|
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<div class="panel-surface p-6">
|
<div class="panel-surface p-6">
|
||||||
@ -52,7 +52,7 @@
|
|||||||
<div class="listings-dashboard-page mx-auto max-w-[1320px] px-4 py-6 md:py-8">
|
<div class="listings-dashboard-page mx-auto max-w-[1320px] px-4 py-6 md:py-8">
|
||||||
<div class="grid gap-6 xl:grid-cols-[300px,minmax(0,1fr)]">
|
<div class="grid gap-6 xl:grid-cols-[300px,minmax(0,1fr)]">
|
||||||
<aside class="listings-dashboard-sidebar space-y-6">
|
<aside class="listings-dashboard-sidebar space-y-6">
|
||||||
@include('panel.partials.sidebar', ['activeMenu' => 'listings'])
|
@include('panel::partials.sidebar', ['activeMenu' => 'listings'])
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="space-y-6">
|
<section class="space-y-6">
|
||||||
@ -73,14 +73,11 @@
|
|||||||
|
|
||||||
<div class="mt-6 rounded-[24px] bg-slate-950 px-5 py-4 text-white shadow-[0_18px_38px_rgba(15,23,42,0.22)]">
|
<div class="mt-6 rounded-[24px] bg-slate-950 px-5 py-4 text-white shadow-[0_18px_38px_rgba(15,23,42,0.22)]">
|
||||||
<p class="text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-slate-300">Profile visibility</p>
|
<p class="text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-slate-300">Profile visibility</p>
|
||||||
<p class="mt-2 text-sm leading-6 text-slate-200">
|
|
||||||
Keep your name and email current so buyers can recognize you quickly in conversations and listing activity.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@include('panel.partials.sidebar', ['activeMenu' => 'profile'])
|
@include('panel::partials.sidebar', ['activeMenu' => 'profile'])
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="space-y-6">
|
<section class="space-y-6">
|
||||||
1
Modules/Panel/resources/views/quick-create.blade.php
Normal file
1
Modules/Panel/resources/views/quick-create.blade.php
Normal file
@ -0,0 +1 @@
|
|||||||
|
@include('panel::partials.quick-create.form')
|
||||||
@ -5,7 +5,7 @@
|
|||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
||||||
@include('panel.partials.sidebar', ['activeMenu' => 'videos'])
|
@include('panel::partials.sidebar', ['activeMenu' => 'videos'])
|
||||||
|
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<div class="panel-surface p-6">
|
<div class="panel-surface p-6">
|
||||||
@ -5,10 +5,10 @@
|
|||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
||||||
@include('panel.partials.sidebar', ['activeMenu' => 'videos'])
|
@include('panel::partials.sidebar', ['activeMenu' => 'videos'])
|
||||||
|
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
@include('panel.partials.page-header', [
|
@include('panel::partials.page-header', [
|
||||||
'title' => 'Videos',
|
'title' => 'Videos',
|
||||||
'description' => 'Upload listing videos and manage processing from one frontend workspace.',
|
'description' => 'Upload listing videos and manage processing from one frontend workspace.',
|
||||||
])
|
])
|
||||||
21
Modules/Panel/routes/web.php
Normal file
21
Modules/Panel/routes/web.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Modules\Panel\App\Http\Controllers\PanelController;
|
||||||
|
|
||||||
|
Route::middleware(['web', 'auth'])->prefix('panel')->name('panel.')->group(function () {
|
||||||
|
Route::get('/', [PanelController::class, 'index'])->name('index');
|
||||||
|
Route::get('/my-listings', [PanelController::class, 'listings'])->name('listings.index');
|
||||||
|
Route::get('/create-listing', [PanelController::class, 'create'])->name('listings.create');
|
||||||
|
Route::get('/my-listings/{listing}/edit', [PanelController::class, 'editListing'])->name('listings.edit');
|
||||||
|
Route::put('/my-listings/{listing}', [PanelController::class, 'updateListing'])->name('listings.update');
|
||||||
|
Route::post('/my-listings/{listing}/remove', [PanelController::class, 'destroyListing'])->name('listings.destroy');
|
||||||
|
Route::post('/my-listings/{listing}/mark-sold', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold');
|
||||||
|
Route::post('/my-listings/{listing}/republish', [PanelController::class, 'republishListing'])->name('listings.republish');
|
||||||
|
Route::get('/videos', [PanelController::class, 'videos'])->name('videos.index');
|
||||||
|
Route::post('/videos', [PanelController::class, 'storeVideo'])->name('videos.store');
|
||||||
|
Route::get('/videos/{video}/edit', [PanelController::class, 'editVideo'])->name('videos.edit');
|
||||||
|
Route::put('/videos/{video}', [PanelController::class, 'updateVideo'])->name('videos.update');
|
||||||
|
Route::delete('/videos/{video}', [PanelController::class, 'destroyVideo'])->name('videos.destroy');
|
||||||
|
Route::get('/my-profile', [PanelController::class, 'profile'])->name('profile.edit');
|
||||||
|
});
|
||||||
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace Modules\S3\Support;
|
namespace Modules\S3\Support;
|
||||||
|
|
||||||
use App\Settings\GeneralSettings;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Modules\Site\App\Settings\GeneralSettings;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class MediaStorage
|
final class MediaStorage
|
||||||
@ -24,8 +24,6 @@ final class MediaStorage
|
|||||||
public static function defaultDriver(): string
|
public static function defaultDriver(): string
|
||||||
{
|
{
|
||||||
return self::coerceDriver(config('media_storage.default_driver'))
|
return self::coerceDriver(config('media_storage.default_driver'))
|
||||||
?? self::coerceDriver(env('MEDIA_DISK'))
|
|
||||||
?? self::coerceDriver(env('FILESYSTEM_DISK'))
|
|
||||||
?? self::DRIVER_S3;
|
?? self::DRIVER_S3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +102,7 @@ final class MediaStorage
|
|||||||
|
|
||||||
config([
|
config([
|
||||||
'filesystems.default' => $disk,
|
'filesystems.default' => $disk,
|
||||||
'filemanager.disk' => env('FILEMANAGER_DISK', $disk),
|
'filemanager.disk' => $disk,
|
||||||
'filament.default_filesystem_disk' => $disk,
|
'filament.default_filesystem_disk' => $disk,
|
||||||
'media-library.disk_name' => $disk,
|
'media-library.disk_name' => $disk,
|
||||||
'video.disk' => $disk,
|
'video.disk' => $disk,
|
||||||
|
|||||||
34
Modules/Site/App/Http/Controllers/HomeController.php
Normal file
34
Modules/Site/App/Http/Controllers/HomeController.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Site\App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Modules\Category\Models\Category;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
use Modules\User\App\Models\User;
|
||||||
|
|
||||||
|
class HomeController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$categories = Category::homeParentCategories();
|
||||||
|
$featuredListings = Listing::homeFeatured();
|
||||||
|
$recentListings = Listing::homeRecent();
|
||||||
|
$listingCount = Listing::activeCount();
|
||||||
|
$categoryCount = Category::activeCount();
|
||||||
|
$userCount = User::totalCount();
|
||||||
|
$favoriteListingIds = auth()->check()
|
||||||
|
? auth()->user()->homeFavoriteListingIds()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return view('site::home', compact(
|
||||||
|
'categories',
|
||||||
|
'featuredListings',
|
||||||
|
'recentListings',
|
||||||
|
'listingCount',
|
||||||
|
'categoryCount',
|
||||||
|
'userCount',
|
||||||
|
'favoriteListingIds',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Modules/Site/App/Http/Controllers/LanguageController.php
Normal file
20
Modules/Site/App/Http/Controllers/LanguageController.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Site\App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
|
class LanguageController extends Controller
|
||||||
|
{
|
||||||
|
public function switch(string $locale): RedirectResponse
|
||||||
|
{
|
||||||
|
$available = config('app.available_locales', ['en']);
|
||||||
|
|
||||||
|
if (in_array($locale, $available, true)) {
|
||||||
|
session(['locale' => $locale]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()->withInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,18 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace Modules\Site\App\Http\Middleware;
|
||||||
|
|
||||||
use App\Support\RequestAppData;
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Modules\Site\App\Support\RequestAppData;
|
||||||
|
|
||||||
class BootstrapAppData
|
class BootstrapAppData
|
||||||
{
|
{
|
||||||
public function __construct(private readonly RequestAppData $requestAppData)
|
public function __construct(private readonly RequestAppData $requestAppData) {}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(Request $request, Closure $next)
|
public function handle(Request $request, Closure $next): mixed
|
||||||
{
|
{
|
||||||
$this->requestAppData->bootstrap();
|
$this->requestAppData->bootstrap();
|
||||||
|
|
||||||
@ -1,19 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
namespace Modules\Site\App\Http\Middleware;
|
||||||
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class SetLocale
|
class SetLocale
|
||||||
{
|
{
|
||||||
public function handle(Request $request, Closure $next)
|
public function handle(Request $request, Closure $next): mixed
|
||||||
{
|
{
|
||||||
$locale = session('locale', config('app.locale'));
|
$locale = session('locale', config('app.locale'));
|
||||||
$available = config('app.available_locales', ['en']);
|
$available = config('app.available_locales', ['en']);
|
||||||
if (!in_array($locale, $available)) {
|
|
||||||
|
if (! in_array($locale, $available, true)) {
|
||||||
$locale = config('app.locale');
|
$locale = config('app.locale');
|
||||||
}
|
}
|
||||||
|
|
||||||
app()->setLocale($locale);
|
app()->setLocale($locale);
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
18
Modules/Site/App/Providers/SiteServiceProvider.php
Normal file
18
Modules/Site/App/Providers/SiteServiceProvider.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Site\App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\View;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class SiteServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$viewPath = module_path('Site', 'resources/views');
|
||||||
|
|
||||||
|
$this->loadRoutesFrom(module_path('Site', 'routes/web.php'));
|
||||||
|
$this->loadViewsFrom($viewPath, 'site');
|
||||||
|
View::addNamespace('app', $viewPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Settings;
|
namespace Modules\Site\App\Settings;
|
||||||
|
|
||||||
use Spatie\LaravelSettings\Settings;
|
use Spatie\LaravelSettings\Settings;
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Support;
|
namespace Modules\Site\App\Support;
|
||||||
|
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Modules\S3\Support\MediaStorage;
|
use Modules\S3\Support\MediaStorage;
|
||||||
@ -1,14 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Support;
|
namespace Modules\Site\App\Support;
|
||||||
|
|
||||||
use App\Settings\GeneralSettings;
|
|
||||||
use Illuminate\Support\Facades\Config;
|
use Illuminate\Support\Facades\Config;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
use Modules\Location\Models\Country;
|
use Modules\Location\Models\Country;
|
||||||
|
use Modules\Location\Support\CountryCodeManager;
|
||||||
use Modules\S3\Support\MediaStorage;
|
use Modules\S3\Support\MediaStorage;
|
||||||
|
use Modules\Site\App\Settings\GeneralSettings;
|
||||||
use Modules\User\App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@ -33,14 +34,14 @@ final class RequestAppData
|
|||||||
$fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD']));
|
$fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD']));
|
||||||
$fallbackDescription = 'Buy and sell everything in your area.';
|
$fallbackDescription = 'Buy and sell everything in your area.';
|
||||||
$fallbackHomeSlides = HomeSlideDefaults::defaults();
|
$fallbackHomeSlides = HomeSlideDefaults::defaults();
|
||||||
$fallbackGoogleMapsApiKey = env('GOOGLE_MAPS_API_KEY');
|
$fallbackGoogleMapsApiKey = config('services.google_maps.api_key');
|
||||||
$fallbackGoogleClientId = env('GOOGLE_CLIENT_ID');
|
$fallbackGoogleClientId = config('services.google.client_id');
|
||||||
$fallbackGoogleClientSecret = env('GOOGLE_CLIENT_SECRET');
|
$fallbackGoogleClientSecret = config('services.google.client_secret');
|
||||||
$fallbackFacebookClientId = env('FACEBOOK_CLIENT_ID');
|
$fallbackFacebookClientId = config('services.facebook.client_id');
|
||||||
$fallbackFacebookClientSecret = env('FACEBOOK_CLIENT_SECRET');
|
$fallbackFacebookClientSecret = config('services.facebook.client_secret');
|
||||||
$fallbackAppleClientId = env('APPLE_CLIENT_ID');
|
$fallbackAppleClientId = config('services.apple.client_id');
|
||||||
$fallbackAppleClientSecret = env('APPLE_CLIENT_SECRET');
|
$fallbackAppleClientSecret = config('services.apple.client_secret');
|
||||||
$fallbackDefaultCountryCode = '+90';
|
$fallbackDefaultCountryCode = (string) config('app.default_country_code', '+90');
|
||||||
$fallbackMediaDriver = MediaStorage::defaultDriver();
|
$fallbackMediaDriver = MediaStorage::defaultDriver();
|
||||||
|
|
||||||
$generalSettings = [
|
$generalSettings = [
|
||||||
@ -59,13 +60,13 @@ final class RequestAppData
|
|||||||
'whatsapp' => null,
|
'whatsapp' => null,
|
||||||
'google_maps_enabled' => false,
|
'google_maps_enabled' => false,
|
||||||
'google_maps_api_key' => $fallbackGoogleMapsApiKey,
|
'google_maps_api_key' => $fallbackGoogleMapsApiKey,
|
||||||
'google_login_enabled' => (bool) env('ENABLE_GOOGLE_LOGIN', false),
|
'google_login_enabled' => (bool) config('services.google.enabled', false),
|
||||||
'google_client_id' => $fallbackGoogleClientId,
|
'google_client_id' => $fallbackGoogleClientId,
|
||||||
'google_client_secret' => $fallbackGoogleClientSecret,
|
'google_client_secret' => $fallbackGoogleClientSecret,
|
||||||
'facebook_login_enabled' => (bool) env('ENABLE_FACEBOOK_LOGIN', false),
|
'facebook_login_enabled' => (bool) config('services.facebook.enabled', false),
|
||||||
'facebook_client_id' => $fallbackFacebookClientId,
|
'facebook_client_id' => $fallbackFacebookClientId,
|
||||||
'facebook_client_secret' => $fallbackFacebookClientSecret,
|
'facebook_client_secret' => $fallbackFacebookClientSecret,
|
||||||
'apple_login_enabled' => (bool) env('ENABLE_APPLE_LOGIN', false),
|
'apple_login_enabled' => (bool) config('services.apple.enabled', false),
|
||||||
'apple_client_id' => $fallbackAppleClientId,
|
'apple_client_id' => $fallbackAppleClientId,
|
||||||
'apple_client_secret' => $fallbackAppleClientSecret,
|
'apple_client_secret' => $fallbackAppleClientSecret,
|
||||||
];
|
];
|
||||||
@ -174,17 +175,7 @@ final class RequestAppData
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Country::query()
|
return Country::headerLocationOptions();
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('name')
|
|
||||||
->get(['id', 'name', 'code'])
|
|
||||||
->map(fn (Country $country): array => [
|
|
||||||
'id' => (int) $country->id,
|
|
||||||
'name' => (string) $country->name,
|
|
||||||
'code' => strtoupper((string) $country->code),
|
|
||||||
])
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -197,20 +188,7 @@ final class RequestAppData
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Category::query()
|
return Category::headerNavigationItems();
|
||||||
->where('is_active', true)
|
|
||||||
->whereNull('parent_id')
|
|
||||||
->orderBy('sort_order')
|
|
||||||
->orderBy('name')
|
|
||||||
->limit(8)
|
|
||||||
->get(['id', 'name', 'icon'])
|
|
||||||
->map(fn (Category $category): array => [
|
|
||||||
'id' => (int) $category->id,
|
|
||||||
'name' => (string) $category->name,
|
|
||||||
'icon_url' => $category->iconUrl(),
|
|
||||||
])
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
11
Modules/Site/module.json
Normal file
11
Modules/Site/module.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Site",
|
||||||
|
"alias": "site",
|
||||||
|
"description": "Site shell, locale, and landing experience",
|
||||||
|
"keywords": [],
|
||||||
|
"priority": 0,
|
||||||
|
"providers": [
|
||||||
|
"Modules\\Site\\App\\Providers\\SiteServiceProvider"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
14
Modules/Site/routes/web.php
Normal file
14
Modules/Site/routes/web.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Modules\Site\App\Http\Controllers\HomeController;
|
||||||
|
use Modules\Site\App\Http\Controllers\LanguageController;
|
||||||
|
|
||||||
|
Route::middleware('web')->group(function () {
|
||||||
|
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||||
|
Route::get('/lang/{locale}', [LanguageController::class, 'switch'])->name('lang.switch');
|
||||||
|
Route::get('/dashboard', fn () => auth()->check()
|
||||||
|
? redirect()->route('panel.listings.index')
|
||||||
|
: redirect()->route('login'))
|
||||||
|
->name('dashboard');
|
||||||
|
});
|
||||||
@ -26,4 +26,18 @@ class Profile extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function detailsForUser(User $user): ?self
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->where('user_id', $user->getKey())
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function phoneForUser(User $user): ?string
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->where('user_id', $user->getKey())
|
||||||
|
->value('phone');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ namespace Modules\User\App\Models;
|
|||||||
use Filament\Models\Contracts\FilamentUser;
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
use Filament\Models\Contracts\HasAvatar;
|
use Filament\Models\Contracts\HasAvatar;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
@ -29,9 +30,9 @@ class User extends Authenticatable implements FilamentUser, HasAvatar
|
|||||||
use HasApiTokens;
|
use HasApiTokens;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HasRoles;
|
use HasRoles;
|
||||||
|
use HasStates;
|
||||||
use LogsActivity;
|
use LogsActivity;
|
||||||
use Notifiable;
|
use Notifiable;
|
||||||
use HasStates;
|
|
||||||
use TwoFactorAuthenticatable;
|
use TwoFactorAuthenticatable;
|
||||||
|
|
||||||
protected $fillable = ['name', 'email', 'password', 'avatar_url', 'status'];
|
protected $fillable = ['name', 'email', 'password', 'avatar_url', 'status'];
|
||||||
@ -219,4 +220,34 @@ class User extends Authenticatable implements FilamentUser, HasAvatar
|
|||||||
'favorites' => $this->savedListingsCount(),
|
'favorites' => $this->savedListingsCount(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function totalCount(): int
|
||||||
|
{
|
||||||
|
return (int) static::query()->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function homeFavoriteListingIds(): array
|
||||||
|
{
|
||||||
|
return $this->favoriteListings()
|
||||||
|
->pluck('listings.id')
|
||||||
|
->map(fn ($id): int => (int) $id)
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function panelListingOptions(): Collection
|
||||||
|
{
|
||||||
|
return $this->listings()
|
||||||
|
->latest('id')
|
||||||
|
->get(['id', 'title', 'status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadPanelProfile(): self
|
||||||
|
{
|
||||||
|
return $this->loadCount([
|
||||||
|
'listings',
|
||||||
|
'favoriteListings',
|
||||||
|
'favoriteSearches',
|
||||||
|
'favoriteSellers',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ class AuthUserSeeder extends Seeder
|
|||||||
],
|
],
|
||||||
));
|
));
|
||||||
|
|
||||||
if (! class_exists(Role::class) || ! Schema::hasTable((new Role())->getTable())) {
|
if (! class_exists(Role::class) || ! Schema::hasTable((new Role)->getTable())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ class Video extends Model
|
|||||||
return $query->orderBy('sort_order')->orderBy('id');
|
return $query->orderBy('sort_order')->orderBy('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopeOwnedByUser(Builder $query, int | string | null $userId): Builder
|
public function scopeOwnedByUser(Builder $query, int|string|null $userId): Builder
|
||||||
{
|
{
|
||||||
return $query->where('user_id', $userId);
|
return $query->where('user_id', $userId);
|
||||||
}
|
}
|
||||||
@ -143,6 +143,19 @@ class Video extends Model
|
|||||||
return ((int) $listing->videos()->max('sort_order')) + 1;
|
return ((int) $listing->videos()->max('sort_order')) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function panelIndexDataForUser(User $user): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'videos' => static::query()
|
||||||
|
->ownedByUser($user->getKey())
|
||||||
|
->with('listing:id,title,user_id')
|
||||||
|
->latest('id')
|
||||||
|
->paginate(10)
|
||||||
|
->withQueryString(),
|
||||||
|
'listingOptions' => $user->panelListingOptions(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function markAsProcessing(): void
|
public function markAsProcessing(): void
|
||||||
{
|
{
|
||||||
if (blank($this->upload_path)) {
|
if (blank($this->upload_path)) {
|
||||||
@ -302,6 +315,11 @@ class Video extends Model
|
|||||||
return number_format($value, $power === 0 ? 0 : 1).' '.$units[$power];
|
return number_format($value, $power === 0 ? 0 : 1).' '.$units[$power];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function assertOwnedBy(User $user): void
|
||||||
|
{
|
||||||
|
abort_unless((int) $this->user_id === (int) $user->getKey(), 403);
|
||||||
|
}
|
||||||
|
|
||||||
public function updateFromPanel(array $attributes): void
|
public function updateFromPanel(array $attributes): void
|
||||||
{
|
{
|
||||||
$this->forceFill([
|
$this->forceFill([
|
||||||
|
|||||||
@ -2,7 +2,4 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
abstract class Controller
|
abstract class Controller {}
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Modules\Listing\Models\Listing;
|
|
||||||
use Modules\Category\Models\Category;
|
|
||||||
use Modules\User\App\Models\User;
|
|
||||||
|
|
||||||
class HomeController extends Controller
|
|
||||||
{
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
$categories = Category::whereNull('parent_id')->where('is_active', true)->get();
|
|
||||||
$featuredListings = Listing::where('status', 'active')->where('is_featured', true)->latest()->take(4)->get();
|
|
||||||
$recentListings = Listing::where('status', 'active')->latest()->take(8)->get();
|
|
||||||
$listingCount = Listing::where('status', 'active')->count();
|
|
||||||
$categoryCount = Category::where('is_active', true)->count();
|
|
||||||
$userCount = User::count();
|
|
||||||
$favoriteListingIds = auth()->check()
|
|
||||||
? auth()->user()->favoriteListings()->pluck('listings.id')->all()
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return view('home', compact(
|
|
||||||
'categories',
|
|
||||||
'featuredListings',
|
|
||||||
'recentListings',
|
|
||||||
'listingCount',
|
|
||||||
'categoryCount',
|
|
||||||
'userCount',
|
|
||||||
'favoriteListingIds',
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class LanguageController extends Controller
|
|
||||||
{
|
|
||||||
public function switch(string $locale)
|
|
||||||
{
|
|
||||||
$available = config('app.available_locales', ['en']);
|
|
||||||
if (in_array($locale, $available)) {
|
|
||||||
session(['locale' => $locale]);
|
|
||||||
}
|
|
||||||
return redirect()->back()->withInput();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,247 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
use Modules\Listing\Models\Listing;
|
|
||||||
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
|
||||||
use Modules\Listing\Support\ListingPanelHelper;
|
|
||||||
use Modules\Video\Enums\VideoStatus;
|
|
||||||
use Modules\Video\Models\Video;
|
|
||||||
|
|
||||||
class PanelController extends Controller
|
|
||||||
{
|
|
||||||
public function index(): RedirectResponse
|
|
||||||
{
|
|
||||||
return redirect()->route('panel.listings.index');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function create(): View
|
|
||||||
{
|
|
||||||
return view('panel.create');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function listings(Request $request): View
|
|
||||||
{
|
|
||||||
$user = $request->user();
|
|
||||||
$search = trim((string) $request->string('search'));
|
|
||||||
$status = (string) $request->string('status', 'all');
|
|
||||||
|
|
||||||
if (! in_array($status, ['all', 'sold', 'expired'], true)) {
|
|
||||||
$status = 'all';
|
|
||||||
}
|
|
||||||
|
|
||||||
$listings = Listing::query()
|
|
||||||
->ownedByUser($user->getKey())
|
|
||||||
->with('category:id,name')
|
|
||||||
->withCount('favoritedByUsers')
|
|
||||||
->withCount('videos')
|
|
||||||
->withCount([
|
|
||||||
'videos as ready_videos_count' => fn ($query) => $query->whereNotNull('path')->where('is_active', true),
|
|
||||||
'videos as pending_videos_count' => fn ($query) => $query->whereIn('status', [
|
|
||||||
VideoStatus::Pending->value,
|
|
||||||
VideoStatus::Processing->value,
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
->searchTerm($search)
|
|
||||||
->forPanelStatus($status)
|
|
||||||
->latest('id')
|
|
||||||
->paginate(10)
|
|
||||||
->withQueryString();
|
|
||||||
|
|
||||||
return view('panel.listings', [
|
|
||||||
'listings' => $listings,
|
|
||||||
'status' => $status,
|
|
||||||
'search' => $search,
|
|
||||||
'counts' => Listing::panelStatusCountsForUser($user->getKey()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function editListing(Request $request, Listing $listing): View
|
|
||||||
{
|
|
||||||
$this->guardListingOwner($request, $listing);
|
|
||||||
|
|
||||||
return view('panel.edit-listing', [
|
|
||||||
'listing' => $listing->load(['category:id,name', 'videos:id,listing_id,title,status,is_active,path,upload_path,duration_seconds,size']),
|
|
||||||
'customFieldValues' => ListingCustomFieldSchemaBuilder::presentableValues(
|
|
||||||
$listing->category_id ? (int) $listing->category_id : null,
|
|
||||||
(array) $listing->custom_fields,
|
|
||||||
),
|
|
||||||
'statusOptions' => Listing::panelStatusOptions(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateListing(Request $request, Listing $listing): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->guardListingOwner($request, $listing);
|
|
||||||
|
|
||||||
$validated = $request->validate([
|
|
||||||
'title' => ['required', 'string', 'max:255'],
|
|
||||||
'description' => ['nullable', 'string', 'max:5000'],
|
|
||||||
'price' => ['nullable', 'numeric', 'min:0'],
|
|
||||||
'status' => ['required', Rule::in(array_keys(Listing::panelStatusOptions()))],
|
|
||||||
'contact_phone' => ['nullable', 'string', 'max:60'],
|
|
||||||
'contact_email' => ['nullable', 'email', 'max:255'],
|
|
||||||
'country' => ['nullable', 'string', 'max:255'],
|
|
||||||
'city' => ['nullable', 'string', 'max:255'],
|
|
||||||
'expires_at' => ['nullable', 'date'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$listing->updateFromPanel($validated + [
|
|
||||||
'currency' => $listing->currency ?: ListingPanelHelper::defaultCurrency(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return redirect()
|
|
||||||
->route('panel.listings.edit', $listing)
|
|
||||||
->with('success', 'Listing updated.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function videos(Request $request): View
|
|
||||||
{
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
return view('panel.videos', [
|
|
||||||
'videos' => Video::query()
|
|
||||||
->ownedByUser($user->getKey())
|
|
||||||
->with('listing:id,title,user_id')
|
|
||||||
->latest('id')
|
|
||||||
->paginate(10)
|
|
||||||
->withQueryString(),
|
|
||||||
'listingOptions' => $user->listings()
|
|
||||||
->latest('id')
|
|
||||||
->get(['id', 'title', 'status']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function storeVideo(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validate([
|
|
||||||
'listing_id' => ['required', 'integer'],
|
|
||||||
'title' => ['nullable', 'string', 'max:255'],
|
|
||||||
'description' => ['nullable', 'string', 'max:2000'],
|
|
||||||
'video_file' => ['required', 'file', 'mimes:mp4,mov,webm,m4v', 'max:256000'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$listing = $request->user()->listings()->whereKey($validated['listing_id'])->firstOrFail();
|
|
||||||
|
|
||||||
$video = Video::createFromUploadedFile($listing, $request->file('video_file'), [
|
|
||||||
'title' => $validated['title'] ?? null,
|
|
||||||
'description' => $validated['description'] ?? null,
|
|
||||||
'sort_order' => Video::nextSortOrderForListing($listing),
|
|
||||||
'is_active' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return redirect()
|
|
||||||
->route('panel.videos.edit', $video)
|
|
||||||
->with('success', 'Video uploaded.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function editVideo(Request $request, Video $video): View
|
|
||||||
{
|
|
||||||
$this->guardVideoOwner($request, $video);
|
|
||||||
|
|
||||||
return view('panel.video-edit', [
|
|
||||||
'video' => $video->load('listing:id,title,user_id'),
|
|
||||||
'listingOptions' => $request->user()->listings()
|
|
||||||
->latest('id')
|
|
||||||
->get(['id', 'title', 'status']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function updateVideo(Request $request, Video $video): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->guardVideoOwner($request, $video);
|
|
||||||
|
|
||||||
$validated = $request->validate([
|
|
||||||
'listing_id' => ['required', 'integer'],
|
|
||||||
'title' => ['nullable', 'string', 'max:255'],
|
|
||||||
'description' => ['nullable', 'string', 'max:2000'],
|
|
||||||
'video_file' => ['nullable', 'file', 'mimes:mp4,mov,webm,m4v', 'max:256000'],
|
|
||||||
'is_active' => ['nullable', 'boolean'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$listing = $request->user()->listings()->whereKey($validated['listing_id'])->firstOrFail();
|
|
||||||
|
|
||||||
$video->updateFromPanel([
|
|
||||||
'listing_id' => $listing->getKey(),
|
|
||||||
'title' => $validated['title'] ?? null,
|
|
||||||
'description' => $validated['description'] ?? null,
|
|
||||||
'video_file' => $request->file('video_file'),
|
|
||||||
'is_active' => $request->boolean('is_active'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return redirect()
|
|
||||||
->route('panel.videos.edit', $video)
|
|
||||||
->with('success', 'Video updated.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroyVideo(Request $request, Video $video): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->guardVideoOwner($request, $video);
|
|
||||||
$video->delete();
|
|
||||||
|
|
||||||
return redirect()
|
|
||||||
->route('panel.videos.index')
|
|
||||||
->with('success', 'Video deleted.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function profile(Request $request): View
|
|
||||||
{
|
|
||||||
$user = $request->user()->loadCount([
|
|
||||||
'listings',
|
|
||||||
'favoriteListings',
|
|
||||||
'favoriteSearches',
|
|
||||||
'favoriteSellers',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return view('panel.profile', [
|
|
||||||
'user' => $user,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroyListing(Request $request, Listing $listing): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->guardListingOwner($request, $listing);
|
|
||||||
$listing->delete();
|
|
||||||
|
|
||||||
return back()->with('success', 'Listing removed.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function markListingAsSold(Request $request, Listing $listing): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->guardListingOwner($request, $listing);
|
|
||||||
$listing->forceFill([
|
|
||||||
'status' => 'sold',
|
|
||||||
])->save();
|
|
||||||
|
|
||||||
return back()->with('success', 'Listing marked as sold.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function republishListing(Request $request, Listing $listing): RedirectResponse
|
|
||||||
{
|
|
||||||
$this->guardListingOwner($request, $listing);
|
|
||||||
$listing->forceFill([
|
|
||||||
'status' => 'active',
|
|
||||||
'expires_at' => now()->addDays(30),
|
|
||||||
])->save();
|
|
||||||
|
|
||||||
return back()->with('success', 'Listing republished.');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function guardListingOwner(Request $request, Listing $listing): void
|
|
||||||
{
|
|
||||||
if ((int) $listing->user_id !== (int) $request->user()->getKey()) {
|
|
||||||
abort(403);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function guardVideoOwner(Request $request, Video $video): void
|
|
||||||
{
|
|
||||||
if ((int) $video->user_id !== (int) $request->user()->getKey()) {
|
|
||||||
abort(403);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\View\Components;
|
|
||||||
|
|
||||||
use Illuminate\View\Component;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class AppLayout extends Component
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the view / contents that represents the component.
|
|
||||||
*/
|
|
||||||
public function render(): View
|
|
||||||
{
|
|
||||||
return view('layouts.app');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
use Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests;
|
|
||||||
use Illuminate\Session\Middleware\StartSession;
|
use Illuminate\Session\Middleware\StartSession;
|
||||||
use Modules\Demo\App\Http\Middleware\ResolveDemoRequest;
|
use Modules\Demo\App\Http\Middleware\ResolveDemoRequest;
|
||||||
|
use Modules\Site\App\Http\Middleware\BootstrapAppData;
|
||||||
|
use Modules\Site\App\Http\Middleware\SetLocale;
|
||||||
|
|
||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
@ -17,13 +19,13 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
ResolveDemoRequest::class,
|
ResolveDemoRequest::class,
|
||||||
\App\Http\Middleware\BootstrapAppData::class,
|
BootstrapAppData::class,
|
||||||
\App\Http\Middleware\SetLocale::class,
|
SetLocale::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$middleware->appendToPriorityList(StartSession::class, ResolveDemoRequest::class);
|
$middleware->appendToPriorityList(StartSession::class, ResolveDemoRequest::class);
|
||||||
$middleware->appendToPriorityList(ResolveDemoRequest::class, \App\Http\Middleware\BootstrapAppData::class);
|
$middleware->appendToPriorityList(ResolveDemoRequest::class, BootstrapAppData::class);
|
||||||
$middleware->appendToPriorityList(\App\Http\Middleware\BootstrapAppData::class, \App\Http\Middleware\SetLocale::class);
|
$middleware->appendToPriorityList(BootstrapAppData::class, SetLocale::class);
|
||||||
$middleware->prependToPriorityList(AuthenticatesRequests::class, ResolveDemoRequest::class);
|
$middleware->prependToPriorityList(AuthenticatesRequests::class, ResolveDemoRequest::class);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
|||||||
@ -1,83 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Application Name
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This value is the name of your application, which will be used when the
|
|
||||||
| framework needs to place the application's name in a notification or
|
|
||||||
| other UI elements where an application name needs to be displayed.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'name' => env('APP_NAME', 'Laravel'),
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Application Environment
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This value determines the "environment" your application is currently
|
|
||||||
| running in. This may determine how you prefer to configure various
|
|
||||||
| services the application utilizes. Set this in your ".env" file.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'env' => env('APP_ENV', 'production'),
|
'env' => env('APP_ENV', 'production'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Application Debug Mode
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When your application is in debug mode, detailed error messages with
|
|
||||||
| stack traces will be shown on every error that occurs within your
|
|
||||||
| application. If disabled, a simple generic error page is shown.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'debug' => (bool) env('APP_DEBUG', false),
|
'debug' => (bool) env('APP_DEBUG', false),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Application URL
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This URL is used by the console to properly generate URLs when using
|
|
||||||
| the Artisan command line tool. You should set this to the root of
|
|
||||||
| the application so that it's available within Artisan commands.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'url' => env('APP_URL', 'http://localhost'),
|
'url' => env('APP_URL', 'http://localhost'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Application Timezone
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may specify the default timezone for your application, which
|
|
||||||
| will be used by the PHP date and date-time functions. The timezone
|
|
||||||
| is set to "UTC" by default as it is suitable for most use cases.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'timezone' => 'UTC',
|
'timezone' => 'UTC',
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Application Locale Configuration
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The application locale determines the default locale that will be used
|
|
||||||
| by Laravel's translation / localization methods. This option can be
|
|
||||||
| set to any locale for which you plan to have translation strings.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'locale' => env('APP_LOCALE', 'en'),
|
'locale' => env('APP_LOCALE', 'en'),
|
||||||
|
|
||||||
'available_locales' => ['en', 'tr', 'ar', 'zh', 'es', 'fr', 'de', 'pt', 'ru', 'ja'],
|
'available_locales' => ['en', 'tr', 'ar', 'zh', 'es', 'fr', 'de', 'pt', 'ru', 'ja'],
|
||||||
@ -87,18 +15,6 @@ return [
|
|||||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||||
|
|
||||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Encryption Key
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This key is utilized by Laravel's encryption services and should be set
|
|
||||||
| to a random, 32 character string to ensure that all encrypted values
|
|
||||||
| are secure. You should do this prior to deploying the application.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'cipher' => 'AES-256-CBC',
|
'cipher' => 'AES-256-CBC',
|
||||||
|
|
||||||
'key' => env('APP_KEY'),
|
'key' => env('APP_KEY'),
|
||||||
@ -108,20 +24,6 @@ return [
|
|||||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Maintenance Mode Driver
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| These configuration options determine the driver used to determine and
|
|
||||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
|
||||||
| allow maintenance mode to be controlled across multiple machines.
|
|
||||||
|
|
|
||||||
| Supported drivers: "file", "cache"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'maintenance' => [
|
'maintenance' => [
|
||||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
|||||||
@ -1,95 +1,22 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Authentication Defaults
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option defines the default authentication "guard" and password
|
|
||||||
| reset "broker" for your application. You may change these values
|
|
||||||
| as required, but they're a perfect start for most applications.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'defaults' => [
|
'defaults' => [
|
||||||
'guard' => env('AUTH_GUARD', 'web'),
|
'guard' => env('AUTH_GUARD', 'web'),
|
||||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Authentication Guards
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Next, you may define every authentication guard for your application.
|
|
||||||
| Of course, a great default configuration has been defined for you
|
|
||||||
| which utilizes session storage plus the Eloquent user provider.
|
|
||||||
|
|
|
||||||
| All authentication guards have a user provider, which defines how the
|
|
||||||
| users are actually retrieved out of your database or other storage
|
|
||||||
| system used by the application. Typically, Eloquent is utilized.
|
|
||||||
|
|
|
||||||
| Supported: "session"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'guards' => [
|
'guards' => [
|
||||||
'web' => [
|
'web' => [
|
||||||
'driver' => 'session',
|
'driver' => 'session',
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| User Providers
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| All authentication guards have a user provider, which defines how the
|
|
||||||
| users are actually retrieved out of your database or other storage
|
|
||||||
| system used by the application. Typically, Eloquent is utilized.
|
|
||||||
|
|
|
||||||
| If you have multiple user tables or models you may configure multiple
|
|
||||||
| providers to represent the model / table. These providers may then
|
|
||||||
| be assigned to any extra authentication guards you have defined.
|
|
||||||
|
|
|
||||||
| Supported: "database", "eloquent"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => env('AUTH_MODEL', Modules\User\App\Models\User::class),
|
'model' => env('AUTH_MODEL', Modules\User\App\Models\User::class),
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
|
||||||
// 'driver' => 'database',
|
|
||||||
// 'table' => 'users',
|
|
||||||
// ],
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Resetting Passwords
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| These configuration options specify the behavior of Laravel's password
|
|
||||||
| reset functionality, including the table utilized for token storage
|
|
||||||
| and the user provider that is invoked to actually retrieve users.
|
|
||||||
|
|
|
||||||
| The expiry time is the number of minutes that each reset token will be
|
|
||||||
| considered valid. This security feature keeps tokens short-lived so
|
|
||||||
| they have less time to be guessed. You may change this as needed.
|
|
||||||
|
|
|
||||||
| The throttle setting is the number of seconds a user must wait before
|
|
||||||
| generating more password reset tokens. This prevents the user from
|
|
||||||
| quickly generating a very large amount of password reset tokens.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'passwords' => [
|
'passwords' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
@ -98,18 +25,6 @@ return [
|
|||||||
'throttle' => 60,
|
'throttle' => 60,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Password Confirmation Timeout
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may define the number of seconds before a password confirmation
|
|
||||||
| window expires and users are asked to re-enter their password via the
|
|
||||||
| confirmation screen. By default, the timeout lasts for three hours.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,33 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Broadcaster
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option controls the default broadcaster that will be used by the
|
|
||||||
| framework when an event needs to be broadcast. You may set this to
|
|
||||||
| any of the connections defined in the "connections" array below.
|
|
||||||
|
|
|
||||||
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('BROADCAST_CONNECTION', 'null'),
|
'default' => env('BROADCAST_CONNECTION', 'null'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Broadcast Connections
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may define all of the broadcast connections that will be used
|
|
||||||
| to broadcast events to other systems or over WebSockets. Samples of
|
|
||||||
| each available type of connection are provided inside this array.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'connections' => [
|
'connections' => [
|
||||||
|
|
||||||
'reverb' => [
|
'reverb' => [
|
||||||
@ -42,7 +16,6 @@ return [
|
|||||||
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
||||||
],
|
],
|
||||||
'client_options' => [
|
'client_options' => [
|
||||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -60,7 +33,6 @@ return [
|
|||||||
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
|
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
|
||||||
],
|
],
|
||||||
'client_options' => [
|
'client_options' => [
|
||||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@ -3,35 +3,7 @@
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Cache Store
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option controls the default cache store that will be used by the
|
|
||||||
| framework. This connection is utilized if another isn't explicitly
|
|
||||||
| specified when running a cache operation inside the application.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('CACHE_STORE', 'database'),
|
'default' => env('CACHE_STORE', 'database'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Cache Stores
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may define all of the cache "stores" for your application as
|
|
||||||
| well as their drivers. You may even define multiple stores for the
|
|
||||||
| same cache driver to group types of items stored in your caches.
|
|
||||||
|
|
|
||||||
| Supported drivers: "array", "database", "file", "memcached",
|
|
||||||
| "redis", "dynamodb", "octane",
|
|
||||||
| "failover", "null"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'stores' => [
|
'stores' => [
|
||||||
|
|
||||||
'array' => [
|
'array' => [
|
||||||
@ -61,7 +33,6 @@ return [
|
|||||||
env('MEMCACHED_PASSWORD'),
|
env('MEMCACHED_PASSWORD'),
|
||||||
],
|
],
|
||||||
'options' => [
|
'options' => [
|
||||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
|
||||||
],
|
],
|
||||||
'servers' => [
|
'servers' => [
|
||||||
[
|
[
|
||||||
@ -100,18 +71,6 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Cache Key Prefix
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
|
||||||
| stores, there might be other applications using the same cache. For
|
|
||||||
| that reason, you may prefix every cache key to avoid collisions.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -3,32 +3,7 @@
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Database Connection Name
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may specify which of the database connections below you wish
|
|
||||||
| to use as your default connection for database operations. This is
|
|
||||||
| the connection which will be utilized unless another connection
|
|
||||||
| is explicitly specified when you execute a query / statement.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Database Connections
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Below are all of the database connections defined for your application.
|
|
||||||
| An example configuration is provided for each database system which
|
|
||||||
| is supported by Laravel. You're free to add / remove connections.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'connections' => [
|
'connections' => [
|
||||||
|
|
||||||
'sqlite' => [
|
'sqlite' => [
|
||||||
@ -124,39 +99,13 @@ return [
|
|||||||
'charset' => env('DB_CHARSET', 'utf8'),
|
'charset' => env('DB_CHARSET', 'utf8'),
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'prefix_indexes' => true,
|
||||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
|
||||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Migration Repository Table
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This table keeps track of all the migrations that have already run for
|
|
||||||
| your application. Using this information, we can determine which of
|
|
||||||
| the migrations on disk haven't actually been run on the database.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'migrations' => [
|
'migrations' => [
|
||||||
'table' => 'migrations',
|
'table' => 'migrations',
|
||||||
'update_date_on_publish' => true,
|
'update_date_on_publish' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Redis Databases
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Redis is an open source, fast, and advanced key-value store that also
|
|
||||||
| provides a richer body of commands than a typical key-value system
|
|
||||||
| such as Memcached. You may define your connection settings here.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'redis' => [
|
'redis' => [
|
||||||
|
|
||||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||||
|
|||||||
@ -1,122 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| File Manager Mode
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The file manager supports two modes:
|
|
||||||
|
|
|
||||||
| - 'database': Files and folders are tracked in a database table.
|
|
||||||
| Metadata, hierarchy, and relationships are stored in the database.
|
|
||||||
| File contents are stored on the configured disk. Best for applications
|
|
||||||
| that need to attach metadata, tags, or relationships to files.
|
|
||||||
|
|
|
||||||
| - 'storage': Files and folders are read directly from a storage disk.
|
|
||||||
| No database is used. The file manager shows the actual file system
|
|
||||||
| structure. Renaming and moving actually rename/move files on the disk.
|
|
||||||
| Best for managing cloud storage (S3, etc.) or local file systems.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'mode' => 'database', // 'database' or 'storage'
|
'mode' => 'database', // 'database' or 'storage'
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Storage Mode Settings
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| These settings only apply when mode is set to 'storage'.
|
|
||||||
|
|
|
||||||
| - disk: The Laravel filesystem disk to use (e.g., 'local', 's3', 'public')
|
|
||||||
| - root: The root path within the disk (empty string for disk root)
|
|
||||||
| - show_hidden: Whether to show hidden files (starting with .)
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'storage_mode' => [
|
'storage_mode' => [
|
||||||
'disk' => env('FILEMANAGER_DISK', env('FILESYSTEM_DISK', 'public')),
|
'disk' => env('FILEMANAGER_DISK', env('FILESYSTEM_DISK', 'public')),
|
||||||
'root' => env('FILEMANAGER_ROOT', ''),
|
'root' => env('FILEMANAGER_ROOT', ''),
|
||||||
'show_hidden' => env('FILEMANAGER_SHOW_HIDDEN', false),
|
'show_hidden' => env('FILEMANAGER_SHOW_HIDDEN', false),
|
||||||
// For S3/MinIO: URL expiration time in minutes for signed URLs
|
|
||||||
'url_expiration' => env('FILEMANAGER_URL_EXPIRATION', 60),
|
'url_expiration' => env('FILEMANAGER_URL_EXPIRATION', 60),
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| File Streaming Settings
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Configure how files are served for preview and download.
|
|
||||||
|
|
|
||||||
| The file manager uses different URL strategies based on the disk:
|
|
||||||
| - S3-compatible disks: Uses temporaryUrl() for pre-signed URLs
|
|
||||||
| - Public disk: Uses direct Storage::url() (works via symlink)
|
|
||||||
| - Local/other disks: Uses signed routes to a streaming controller
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'streaming' => [
|
'streaming' => [
|
||||||
// URL generation strategy:
|
|
||||||
// - 'auto': Automatically detect best strategy per disk (recommended)
|
|
||||||
// - 'signed_route': Always use signed routes to streaming controller
|
|
||||||
// - 'direct': Always use Storage::url() (only works for public disk)
|
|
||||||
'url_strategy' => env('FILEMANAGER_URL_STRATEGY', 'auto'),
|
'url_strategy' => env('FILEMANAGER_URL_STRATEGY', 'auto'),
|
||||||
|
|
||||||
// URL expiration in minutes (for signed URLs and S3 temporary URLs)
|
|
||||||
'url_expiration' => env('FILEMANAGER_URL_EXPIRATION', 60),
|
'url_expiration' => env('FILEMANAGER_URL_EXPIRATION', 60),
|
||||||
|
|
||||||
// Route prefix for streaming endpoints
|
|
||||||
'route_prefix' => env('FILEMANAGER_ROUTE_PREFIX', 'filemanager'),
|
'route_prefix' => env('FILEMANAGER_ROUTE_PREFIX', 'filemanager'),
|
||||||
|
|
||||||
// Middleware applied to streaming routes
|
|
||||||
'middleware' => ['web'],
|
'middleware' => ['web'],
|
||||||
|
|
||||||
// Disks that should always use signed routes (even if public)
|
|
||||||
// Useful if you want extra security for certain disks
|
|
||||||
'force_signed_disks' => [],
|
'force_signed_disks' => [],
|
||||||
|
|
||||||
// Disks that are publicly accessible via URL (override auto-detection)
|
|
||||||
// Files on these disks can be accessed directly without streaming
|
|
||||||
'public_disks' => ['public'],
|
'public_disks' => ['public'],
|
||||||
|
|
||||||
// Disks that don't require authentication for streaming access
|
|
||||||
// Use with caution - files on these disks can be accessed without login
|
|
||||||
// Note: Signed URLs are still required, this just skips the auth check
|
|
||||||
'public_access_disks' => [],
|
'public_access_disks' => [],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| File System Item Model (Database Mode)
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This is the model that represents files and folders in your application.
|
|
||||||
| Only used when mode is 'database'.
|
|
||||||
| It must implement the MWGuerra\FileManager\Contracts\FileSystemItemInterface.
|
|
||||||
|
|
|
||||||
| The package provides a default model. You can extend it or create your own:
|
|
||||||
|
|
|
||||||
| Option 1: Use the package model directly (default)
|
|
||||||
| 'model' => \MWGuerra\FileManager\Models\FileSystemItem::class,
|
|
||||||
|
|
|
||||||
| Option 2: Extend the package model in your app
|
|
||||||
| 'model' => \App\Models\FileSystemItem::class,
|
|
||||||
| // where App\Models\FileSystemItem extends MWGuerra\FileManager\Models\FileSystemItem
|
|
||||||
|
|
|
||||||
| Option 3: Create your own model implementing FileSystemItemInterface
|
|
||||||
| 'model' => \App\Models\CustomFileModel::class,
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'model' => \MWGuerra\FileManager\Models\FileSystemItem::class,
|
'model' => \MWGuerra\FileManager\Models\FileSystemItem::class,
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| File Manager Page (Database Mode)
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Configure the File Manager page which uses database mode to track
|
|
||||||
| files with metadata, hierarchy, and relationships.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'file_manager' => [
|
'file_manager' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'navigation' => [
|
'navigation' => [
|
||||||
@ -126,16 +27,6 @@ return [
|
|||||||
'group' => 'FileManager',
|
'group' => 'FileManager',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| File System Page (Storage Mode)
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Configure the File System page which shows files directly from the
|
|
||||||
| storage disk without using the database.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'file_system' => [
|
'file_system' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'navigation' => [
|
'navigation' => [
|
||||||
@ -145,46 +36,16 @@ return [
|
|||||||
'group' => 'FileManager',
|
'group' => 'FileManager',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Schema Example Page
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Enable or disable the Schema Example page which demonstrates
|
|
||||||
| how to embed the file manager components into Filament forms.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'schema_example' => [
|
'schema_example' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Upload Settings
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Configure upload settings for the file manager.
|
|
||||||
|
|
|
||||||
| Note: You may also need to adjust PHP settings in php.ini:
|
|
||||||
| - upload_max_filesize (default: 2M)
|
|
||||||
| - post_max_size (default: 8M)
|
|
||||||
| - max_execution_time (default: 30)
|
|
||||||
|
|
|
||||||
| For Livewire temporary uploads, also check config/livewire.php:
|
|
||||||
| - temporary_file_upload.rules (default: max:12288 = 12MB)
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'upload' => [
|
'upload' => [
|
||||||
'disk' => env('FILEMANAGER_DISK', env('FILESYSTEM_DISK', 'public')),
|
'disk' => env('FILEMANAGER_DISK', env('FILESYSTEM_DISK', 'public')),
|
||||||
'directory' => env('FILEMANAGER_UPLOAD_DIR', 'uploads'),
|
'directory' => env('FILEMANAGER_UPLOAD_DIR', 'uploads'),
|
||||||
'max_file_size' => 100 * 1024, // 100 MB in kilobytes
|
'max_file_size' => 100 * 1024, // 100 MB in kilobytes
|
||||||
'allowed_mimes' => [
|
'allowed_mimes' => [
|
||||||
// Videos
|
|
||||||
'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo',
|
'video/mp4', 'video/webm', 'video/ogg', 'video/quicktime', 'video/x-msvideo',
|
||||||
// Images (SVG excluded by default - can contain scripts)
|
|
||||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||||
// Documents
|
|
||||||
'application/pdf',
|
'application/pdf',
|
||||||
'application/msword',
|
'application/msword',
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
@ -193,52 +54,25 @@ return [
|
|||||||
'application/vnd.ms-powerpoint',
|
'application/vnd.ms-powerpoint',
|
||||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
'text/plain',
|
'text/plain',
|
||||||
// Audio
|
|
||||||
'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/flac',
|
'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/flac',
|
||||||
// Archives
|
|
||||||
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
|
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Security Settings
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Configure security settings to prevent malicious file uploads and access.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'security' => [
|
'security' => [
|
||||||
// Dangerous extensions that should NEVER be uploaded (executable files)
|
|
||||||
'blocked_extensions' => [
|
'blocked_extensions' => [
|
||||||
// Server-side scripts
|
|
||||||
'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar',
|
'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar',
|
||||||
'pl', 'py', 'pyc', 'pyo', 'rb', 'sh', 'bash', 'zsh', 'cgi',
|
'pl', 'py', 'pyc', 'pyo', 'rb', 'sh', 'bash', 'zsh', 'cgi',
|
||||||
'asp', 'aspx', 'jsp', 'jspx', 'cfm', 'cfc',
|
'asp', 'aspx', 'jsp', 'jspx', 'cfm', 'cfc',
|
||||||
// Executables
|
|
||||||
'exe', 'msi', 'dll', 'com', 'bat', 'cmd', 'vbs', 'vbe',
|
'exe', 'msi', 'dll', 'com', 'bat', 'cmd', 'vbs', 'vbe',
|
||||||
'js', 'jse', 'ws', 'wsf', 'wsc', 'wsh', 'ps1', 'psm1',
|
'js', 'jse', 'ws', 'wsf', 'wsc', 'wsh', 'ps1', 'psm1',
|
||||||
// Other dangerous
|
|
||||||
'htaccess', 'htpasswd', 'ini', 'log', 'sql', 'env',
|
'htaccess', 'htpasswd', 'ini', 'log', 'sql', 'env',
|
||||||
'pem', 'key', 'crt', 'cer',
|
'pem', 'key', 'crt', 'cer',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Files that can contain embedded scripts (XSS risk when served inline)
|
|
||||||
'sanitize_extensions' => ['svg', 'html', 'htm', 'xml'],
|
'sanitize_extensions' => ['svg', 'html', 'htm', 'xml'],
|
||||||
|
|
||||||
// Validate MIME type matches extension (prevents spoofing)
|
|
||||||
'validate_mime' => true,
|
'validate_mime' => true,
|
||||||
|
|
||||||
// Rename files to prevent execution (adds random prefix)
|
|
||||||
'rename_uploads' => false,
|
'rename_uploads' => false,
|
||||||
|
|
||||||
// Strip potentially dangerous characters from filenames
|
|
||||||
'sanitize_filenames' => true,
|
'sanitize_filenames' => true,
|
||||||
|
|
||||||
// Maximum filename length
|
|
||||||
'max_filename_length' => 255,
|
'max_filename_length' => 255,
|
||||||
|
|
||||||
// Patterns blocked in filenames (regex)
|
|
||||||
'blocked_filename_patterns' => [
|
'blocked_filename_patterns' => [
|
||||||
'/\.{2,}/', // Multiple dots (path traversal)
|
'/\.{2,}/', // Multiple dots (path traversal)
|
||||||
'/^\./', // Hidden files
|
'/^\./', // Hidden files
|
||||||
@ -246,27 +80,8 @@ return [
|
|||||||
'/[<>:"|?*]/', // Windows reserved characters
|
'/[<>:"|?*]/', // Windows reserved characters
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Authorization Settings
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Configure authorization for file manager operations.
|
|
||||||
|
|
|
||||||
| When enabled, the package will check permissions before allowing operations.
|
|
||||||
| You can specify permission names that will be checked via the user's can() method.
|
|
||||||
|
|
|
||||||
| To customize authorization logic, extend FileSystemItemPolicy and register
|
|
||||||
| your custom policy in your application's AuthServiceProvider.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'authorization' => [
|
'authorization' => [
|
||||||
// Enable/disable authorization checks (set to false during development)
|
|
||||||
'enabled' => env('FILEMANAGER_AUTH_ENABLED', true),
|
'enabled' => env('FILEMANAGER_AUTH_ENABLED', true),
|
||||||
|
|
||||||
// Permission names to check (uses user->can() method)
|
|
||||||
// Set to null to skip permission check and just require authentication
|
|
||||||
'permissions' => [
|
'permissions' => [
|
||||||
'view_any' => null, // Access file manager page
|
'view_any' => null, // Access file manager page
|
||||||
'view' => null, // View/preview files
|
'view' => null, // View/preview files
|
||||||
@ -276,55 +91,15 @@ return [
|
|||||||
'delete_any' => null, // Bulk delete
|
'delete_any' => null, // Bulk delete
|
||||||
'download' => null, // Download files
|
'download' => null, // Download files
|
||||||
],
|
],
|
||||||
|
|
||||||
// The policy class to use (can be overridden with custom implementation)
|
|
||||||
'policy' => \MWGuerra\FileManager\Policies\FileSystemItemPolicy::class,
|
'policy' => \MWGuerra\FileManager\Policies\FileSystemItemPolicy::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Panel Sidebar Settings
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Configure the file manager folder tree that can be rendered in the
|
|
||||||
| Filament panel sidebar using render hooks.
|
|
||||||
|
|
|
||||||
| - enabled: Enable/disable the sidebar folder tree
|
|
||||||
| - root_label: Label for the root folder (e.g., "Root", "/", "Home")
|
|
||||||
| - heading: Heading text shown above the folder tree
|
|
||||||
| - show_in_file_manager: Show the sidebar within the file manager page
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'sidebar' => [
|
'sidebar' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'root_label' => env('FILEMANAGER_SIDEBAR_ROOT_LABEL', 'Root'),
|
'root_label' => env('FILEMANAGER_SIDEBAR_ROOT_LABEL', 'Root'),
|
||||||
'heading' => env('FILEMANAGER_SIDEBAR_HEADING', 'Folders'),
|
'heading' => env('FILEMANAGER_SIDEBAR_HEADING', 'Folders'),
|
||||||
'show_in_file_manager' => true,
|
'show_in_file_manager' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| File Types
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Configure which file types are enabled and register custom file types.
|
|
||||||
|
|
|
||||||
| Built-in types can be disabled by setting their value to false.
|
|
||||||
| Custom types can be added by listing their fully-qualified class names.
|
|
||||||
|
|
|
||||||
| Each custom type class must implement FileTypeContract or extend
|
|
||||||
| AbstractFileType from MWGuerra\FileManager\FileTypes.
|
|
||||||
|
|
|
||||||
| Example of registering custom types:
|
|
||||||
|
|
|
||||||
| 'custom' => [
|
|
||||||
| \App\FileTypes\ThreeDModelFileType::class,
|
|
||||||
| \App\FileTypes\EbookFileType::class,
|
|
||||||
| ],
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'file_types' => [
|
'file_types' => [
|
||||||
// Built-in types (set to false to disable)
|
|
||||||
'video' => true,
|
'video' => true,
|
||||||
'image' => true,
|
'image' => true,
|
||||||
'audio' => true,
|
'audio' => true,
|
||||||
@ -332,10 +107,7 @@ return [
|
|||||||
'text' => true,
|
'text' => true,
|
||||||
'document' => true,
|
'document' => true,
|
||||||
'archive' => true,
|
'archive' => true,
|
||||||
|
|
||||||
// Custom file types (fully-qualified class names)
|
|
||||||
'custom' => [
|
'custom' => [
|
||||||
// \App\FileTypes\ThreeDModelFileType::class,
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,33 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Filesystem Disk
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may specify the default filesystem disk that should be used
|
|
||||||
| by the framework. The "local" disk, as well as a variety of cloud
|
|
||||||
| based disks are available to your application for file storage.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('FILESYSTEM_DISK', env('MEDIA_DISK', 's3')),
|
'default' => env('FILESYSTEM_DISK', env('MEDIA_DISK', 's3')),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Filesystem Disks
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Below you may configure as many filesystem disks as necessary, and you
|
|
||||||
| may even configure multiple disks for the same driver. Examples for
|
|
||||||
| most supported storage drivers are configured here for reference.
|
|
||||||
|
|
|
||||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'disks' => [
|
'disks' => [
|
||||||
|
|
||||||
'local' => [
|
'local' => [
|
||||||
@ -62,18 +36,6 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Symbolic Links
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may configure the symbolic links that will be created when the
|
|
||||||
| `storage:link` Artisan command is executed. The array keys should be
|
|
||||||
| the locations of the links and the values should be their targets.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'links' => [
|
'links' => [
|
||||||
public_path('storage') => storage_path('app/public'),
|
public_path('storage') => storage_path('app/public'),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -6,50 +6,11 @@ use Monolog\Handler\SyslogUdpHandler;
|
|||||||
use Monolog\Processor\PsrLogMessageProcessor;
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Log Channel
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option defines the default log channel that is utilized to write
|
|
||||||
| messages to your logs. The value provided here should match one of
|
|
||||||
| the channels present in the list of "channels" configured below.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('LOG_CHANNEL', 'stack'),
|
'default' => env('LOG_CHANNEL', 'stack'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Deprecations Log Channel
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option controls the log channel that should be used to log warnings
|
|
||||||
| regarding deprecated PHP and library features. This allows you to get
|
|
||||||
| your application ready for upcoming major versions of dependencies.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'deprecations' => [
|
'deprecations' => [
|
||||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Log Channels
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may configure the log channels for your application. Laravel
|
|
||||||
| utilizes the Monolog PHP logging library, which includes a variety
|
|
||||||
| of powerful log handlers and formatters that you're free to use.
|
|
||||||
|
|
|
||||||
| Available drivers: "single", "daily", "slack", "syslog",
|
|
||||||
| "errorlog", "monolog", "custom", "stack"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'channels' => [
|
'channels' => [
|
||||||
|
|
||||||
'stack' => [
|
'stack' => [
|
||||||
|
|||||||
@ -1,40 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Mailer
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option controls the default mailer that is used to send all email
|
|
||||||
| messages unless another mailer is explicitly specified when sending
|
|
||||||
| the message. All additional mailers can be configured within the
|
|
||||||
| "mailers" array. Examples of each type of mailer are provided.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('MAIL_MAILER', 'log'),
|
'default' => env('MAIL_MAILER', 'log'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Mailer Configurations
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may configure all of the mailers used by your application plus
|
|
||||||
| their respective settings. Several examples have been configured for
|
|
||||||
| you and you are free to add your own as your application requires.
|
|
||||||
|
|
|
||||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
|
||||||
| when delivering an email. You may specify which one you're using for
|
|
||||||
| your mailers below. You may also add additional mailers if needed.
|
|
||||||
|
|
|
||||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
|
||||||
| "postmark", "resend", "log", "array",
|
|
||||||
| "failover", "roundrobin"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'mailers' => [
|
'mailers' => [
|
||||||
|
|
||||||
'smtp' => [
|
'smtp' => [
|
||||||
@ -55,10 +22,6 @@ return [
|
|||||||
|
|
||||||
'postmark' => [
|
'postmark' => [
|
||||||
'transport' => 'postmark',
|
'transport' => 'postmark',
|
||||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
|
||||||
// 'client' => [
|
|
||||||
// 'timeout' => 5,
|
|
||||||
// ],
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'resend' => [
|
'resend' => [
|
||||||
@ -98,18 +61,6 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Global "From" Address
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| You may wish for all emails sent by your application to be sent from
|
|
||||||
| the same address. Here you may specify a name and address that is
|
|
||||||
| used globally for all emails that are sent by your application.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'from' => [
|
'from' => [
|
||||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||||
|
|||||||
@ -4,25 +4,7 @@ use Nwidart\Modules\Activators\FileActivator;
|
|||||||
use Nwidart\Modules\Providers\ConsoleServiceProvider;
|
use Nwidart\Modules\Providers\ConsoleServiceProvider;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Module Namespace
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Default module namespace.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'namespace' => 'Modules',
|
'namespace' => 'Modules',
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Module Stubs
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Default module stubs.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'stubs' => [
|
'stubs' => [
|
||||||
'enabled' => false,
|
'enabled' => false,
|
||||||
'path' => base_path('vendor/nwidart/laravel-modules/src/Commands/stubs'),
|
'path' => base_path('vendor/nwidart/laravel-modules/src/Commands/stubs'),
|
||||||
@ -60,57 +42,11 @@ return [
|
|||||||
'gitkeep' => true,
|
'gitkeep' => true,
|
||||||
],
|
],
|
||||||
'paths' => [
|
'paths' => [
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Modules path
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This path is used to save the generated module.
|
|
||||||
| This path will also be added automatically to the list of scanned folders.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'modules' => base_path('Modules'),
|
'modules' => base_path('Modules'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Modules assets path
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may update the modules' assets path.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'assets' => public_path('modules'),
|
'assets' => public_path('modules'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| The migrations' path
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Where you run the 'module:publish-migration' command, where do you publish the
|
|
||||||
| the migration files?
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'migration' => base_path('database/migrations'),
|
'migration' => base_path('database/migrations'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| The app path
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| app folder name
|
|
||||||
| for example can change it to 'src' or 'App'
|
|
||||||
*/
|
|
||||||
'app_folder' => '',
|
'app_folder' => '',
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Generator path
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Customise the paths where the folders will be generated.
|
|
||||||
| Setting the generate key to false will not generate that folder
|
|
||||||
*/
|
|
||||||
'generator' => [
|
'generator' => [
|
||||||
// app/
|
|
||||||
'actions' => ['path' => 'app/Actions', 'generate' => false],
|
'actions' => ['path' => 'app/Actions', 'generate' => false],
|
||||||
'casts' => ['path' => 'app/Casts', 'generate' => false],
|
'casts' => ['path' => 'app/Casts', 'generate' => false],
|
||||||
'channels' => ['path' => 'app/Broadcasting', 'generate' => false],
|
'channels' => ['path' => 'app/Broadcasting', 'generate' => false],
|
||||||
@ -137,107 +73,36 @@ return [
|
|||||||
'services' => ['path' => 'app/Services', 'generate' => false],
|
'services' => ['path' => 'app/Services', 'generate' => false],
|
||||||
'scopes' => ['path' => 'app/Models/Scopes', 'generate' => false],
|
'scopes' => ['path' => 'app/Models/Scopes', 'generate' => false],
|
||||||
'traits' => ['path' => 'app/Traits', 'generate' => false],
|
'traits' => ['path' => 'app/Traits', 'generate' => false],
|
||||||
|
|
||||||
// app/Http/
|
|
||||||
'controller' => ['path' => 'app/Http/Controllers', 'generate' => true],
|
'controller' => ['path' => 'app/Http/Controllers', 'generate' => true],
|
||||||
'filter' => ['path' => 'app/Http/Middleware', 'generate' => false],
|
'filter' => ['path' => 'app/Http/Middleware', 'generate' => false],
|
||||||
'request' => ['path' => 'app/Http/Requests', 'generate' => false],
|
'request' => ['path' => 'app/Http/Requests', 'generate' => false],
|
||||||
|
|
||||||
// config/
|
|
||||||
'config' => ['path' => 'config', 'generate' => true],
|
'config' => ['path' => 'config', 'generate' => true],
|
||||||
|
|
||||||
// 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' => ['path' => 'lang', 'generate' => false],
|
'lang' => ['path' => 'lang', 'generate' => false],
|
||||||
|
|
||||||
// resource/
|
|
||||||
'assets' => ['path' => 'resources/assets', 'generate' => true],
|
'assets' => ['path' => 'resources/assets', 'generate' => true],
|
||||||
'component-view' => ['path' => 'resources/views/components', 'generate' => false],
|
'component-view' => ['path' => 'resources/views/components', 'generate' => false],
|
||||||
'views' => ['path' => 'resources/views', 'generate' => true],
|
'views' => ['path' => 'resources/views', 'generate' => true],
|
||||||
|
|
||||||
// routes/
|
|
||||||
'routes' => ['path' => 'routes', 'generate' => true],
|
'routes' => ['path' => 'routes', 'generate' => true],
|
||||||
|
|
||||||
// tests/
|
|
||||||
'test-feature' => ['path' => 'tests/Feature', 'generate' => true],
|
'test-feature' => ['path' => 'tests/Feature', 'generate' => true],
|
||||||
'test-unit' => ['path' => 'tests/Unit', 'generate' => true],
|
'test-unit' => ['path' => 'tests/Unit', 'generate' => true],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Auto Discover of Modules
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you configure auto discover of module
|
|
||||||
| This is useful for simplify module providers.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'auto-discover' => [
|
'auto-discover' => [
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Migrations
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option for register migration automatically.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'migrations' => true,
|
'migrations' => true,
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Translations
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option for register lang file automatically.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'translations' => false,
|
'translations' => false,
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Package commands
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you can define which commands will be visible and used in your
|
|
||||||
| application. You can add your own commands to merge section.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'commands' => ConsoleServiceProvider::defaultCommands()
|
'commands' => ConsoleServiceProvider::defaultCommands()
|
||||||
->merge([
|
->merge([
|
||||||
// New commands go here
|
|
||||||
])->toArray(),
|
])->toArray(),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Scan Path
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you define which folder will be scanned. By default will scan vendor
|
|
||||||
| directory. This is useful if you host the package in packagist website.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'scan' => [
|
'scan' => [
|
||||||
'enabled' => false,
|
'enabled' => false,
|
||||||
'paths' => [
|
'paths' => [
|
||||||
base_path('vendor/*/*'),
|
base_path('vendor/*/*'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Composer File Template
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here is the config for the composer.json file, generated by this package
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
'composer' => [
|
'composer' => [
|
||||||
'vendor' => env('MODULE_VENDOR', 'nwidart'),
|
'vendor' => env('MODULE_VENDOR', 'nwidart'),
|
||||||
'author' => [
|
'author' => [
|
||||||
@ -246,31 +111,10 @@ return [
|
|||||||
],
|
],
|
||||||
'composer-output' => false,
|
'composer-output' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Choose what laravel-modules will register as custom namespaces.
|
|
||||||
| Setting one to false will require you to register that part
|
|
||||||
| in your own Service Provider class.
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
'register' => [
|
'register' => [
|
||||||
'translations' => true,
|
'translations' => true,
|
||||||
/**
|
|
||||||
* load files on boot or register method
|
|
||||||
*/
|
|
||||||
'files' => 'register',
|
'files' => 'register',
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Activators
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| You can define new types of activators here, file, database, etc. The only
|
|
||||||
| required parameter is 'class'.
|
|
||||||
| The file activator will store the activation status in storage/installed_modules
|
|
||||||
*/
|
|
||||||
'activators' => [
|
'activators' => [
|
||||||
'file' => [
|
'file' => [
|
||||||
'class' => FileActivator::class,
|
'class' => FileActivator::class,
|
||||||
|
|||||||
1843
config/money.php
1843
config/money.php
File diff suppressed because it is too large
Load Diff
@ -3,200 +3,37 @@
|
|||||||
return [
|
return [
|
||||||
|
|
||||||
'models' => [
|
'models' => [
|
||||||
|
|
||||||
/*
|
|
||||||
* When using the "HasPermissions" trait from this package, we need to know which
|
|
||||||
* Eloquent model should be used to retrieve your permissions. Of course, it
|
|
||||||
* is often just the "Permission" model but you may use whatever you like.
|
|
||||||
*
|
|
||||||
* The model you want to use as a Permission model needs to implement the
|
|
||||||
* `Spatie\Permission\Contracts\Permission` contract.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'permission' => Spatie\Permission\Models\Permission::class,
|
'permission' => Spatie\Permission\Models\Permission::class,
|
||||||
|
|
||||||
/*
|
|
||||||
* When using the "HasRoles" trait from this package, we need to know which
|
|
||||||
* Eloquent model should be used to retrieve your roles. Of course, it
|
|
||||||
* is often just the "Role" model but you may use whatever you like.
|
|
||||||
*
|
|
||||||
* The model you want to use as a Role model needs to implement the
|
|
||||||
* `Spatie\Permission\Contracts\Role` contract.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'role' => Spatie\Permission\Models\Role::class,
|
'role' => Spatie\Permission\Models\Role::class,
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'table_names' => [
|
'table_names' => [
|
||||||
|
|
||||||
/*
|
|
||||||
* When using the "HasRoles" trait from this package, we need to know which
|
|
||||||
* table should be used to retrieve your roles. We have chosen a basic
|
|
||||||
* default value but you may easily change it to any table you like.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'roles' => 'roles',
|
'roles' => 'roles',
|
||||||
|
|
||||||
/*
|
|
||||||
* When using the "HasPermissions" trait from this package, we need to know which
|
|
||||||
* table should be used to retrieve your permissions. We have chosen a basic
|
|
||||||
* default value but you may easily change it to any table you like.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'permissions' => 'permissions',
|
'permissions' => 'permissions',
|
||||||
|
|
||||||
/*
|
|
||||||
* When using the "HasPermissions" trait from this package, we need to know which
|
|
||||||
* table should be used to retrieve your models permissions. We have chosen a
|
|
||||||
* basic default value but you may easily change it to any table you like.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'model_has_permissions' => 'model_has_permissions',
|
'model_has_permissions' => 'model_has_permissions',
|
||||||
|
|
||||||
/*
|
|
||||||
* When using the "HasRoles" trait from this package, we need to know which
|
|
||||||
* table should be used to retrieve your models roles. We have chosen a
|
|
||||||
* basic default value but you may easily change it to any table you like.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'model_has_roles' => 'model_has_roles',
|
'model_has_roles' => 'model_has_roles',
|
||||||
|
|
||||||
/*
|
|
||||||
* When using the "HasRoles" trait from this package, we need to know which
|
|
||||||
* table should be used to retrieve your roles permissions. We have chosen a
|
|
||||||
* basic default value but you may easily change it to any table you like.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'role_has_permissions' => 'role_has_permissions',
|
'role_has_permissions' => 'role_has_permissions',
|
||||||
],
|
],
|
||||||
|
|
||||||
'column_names' => [
|
'column_names' => [
|
||||||
/*
|
|
||||||
* Change this if you want to name the related pivots other than defaults
|
|
||||||
*/
|
|
||||||
'role_pivot_key' => null, // default 'role_id',
|
'role_pivot_key' => null, // default 'role_id',
|
||||||
'permission_pivot_key' => null, // default 'permission_id',
|
'permission_pivot_key' => null, // default 'permission_id',
|
||||||
|
|
||||||
/*
|
|
||||||
* Change this if you want to name the related model primary key other than
|
|
||||||
* `model_id`.
|
|
||||||
*
|
|
||||||
* For example, this would be nice if your primary keys are all UUIDs. In
|
|
||||||
* that case, name this `model_uuid`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'model_morph_key' => 'model_id',
|
'model_morph_key' => 'model_id',
|
||||||
|
|
||||||
/*
|
|
||||||
* Change this if you want to use the teams feature and your related model's
|
|
||||||
* foreign key is other than `team_id`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'team_foreign_key' => 'team_id',
|
'team_foreign_key' => 'team_id',
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
* When set to true, the method for checking permissions will be registered on the gate.
|
|
||||||
* Set this to false if you want to implement custom logic for checking permissions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'register_permission_check_method' => true,
|
'register_permission_check_method' => true,
|
||||||
|
|
||||||
/*
|
|
||||||
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
|
||||||
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
|
||||||
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
|
||||||
*/
|
|
||||||
'register_octane_reset_listener' => false,
|
'register_octane_reset_listener' => false,
|
||||||
|
|
||||||
/*
|
|
||||||
* Events will fire when a role or permission is assigned/unassigned:
|
|
||||||
* \Spatie\Permission\Events\RoleAttached
|
|
||||||
* \Spatie\Permission\Events\RoleDetached
|
|
||||||
* \Spatie\Permission\Events\PermissionAttached
|
|
||||||
* \Spatie\Permission\Events\PermissionDetached
|
|
||||||
*
|
|
||||||
* To enable, set to true, and then create listeners to watch these events.
|
|
||||||
*/
|
|
||||||
'events_enabled' => false,
|
'events_enabled' => false,
|
||||||
|
|
||||||
/*
|
|
||||||
* Teams Feature.
|
|
||||||
* When set to true the package implements teams using the 'team_foreign_key'.
|
|
||||||
* If you want the migrations to register the 'team_foreign_key', you must
|
|
||||||
* set this to true before doing the migration.
|
|
||||||
* If you already did the migration then you must make a new migration to also
|
|
||||||
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
|
||||||
* (view the latest version of this package's migration file)
|
|
||||||
*/
|
|
||||||
|
|
||||||
'teams' => false,
|
'teams' => false,
|
||||||
|
|
||||||
/*
|
|
||||||
* The class to use to resolve the permissions team id
|
|
||||||
*/
|
|
||||||
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
|
'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
|
||||||
|
|
||||||
/*
|
|
||||||
* Passport Client Credentials Grant
|
|
||||||
* When set to true the package will use Passports Client to check permissions
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use_passport_client_credentials' => false,
|
'use_passport_client_credentials' => false,
|
||||||
|
|
||||||
/*
|
|
||||||
* When set to true, the required permission names are added to exception messages.
|
|
||||||
* This could be considered an information leak in some contexts, so the default
|
|
||||||
* setting is false here for optimum safety.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'display_permission_in_exception' => false,
|
'display_permission_in_exception' => false,
|
||||||
|
|
||||||
/*
|
|
||||||
* When set to true, the required role names are added to exception messages.
|
|
||||||
* This could be considered an information leak in some contexts, so the default
|
|
||||||
* setting is false here for optimum safety.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'display_role_in_exception' => false,
|
'display_role_in_exception' => false,
|
||||||
|
|
||||||
/*
|
|
||||||
* By default wildcard permission lookups are disabled.
|
|
||||||
* See documentation to understand supported syntax.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'enable_wildcard_permission' => false,
|
'enable_wildcard_permission' => false,
|
||||||
|
|
||||||
/*
|
|
||||||
* The class to use for interpreting wildcard permissions.
|
|
||||||
* If you need to modify delimiters, override the class and specify its name here.
|
|
||||||
*/
|
|
||||||
// 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
|
||||||
|
|
||||||
/* Cache-specific settings */
|
|
||||||
|
|
||||||
'cache' => [
|
'cache' => [
|
||||||
|
|
||||||
/*
|
|
||||||
* By default all permissions are cached for 24 hours to speed up performance.
|
|
||||||
* When permissions or roles are updated the cache is flushed automatically.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
|
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
|
||||||
|
|
||||||
/*
|
|
||||||
* The cache key used to store all permissions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'key' => 'spatie.permission.cache',
|
'key' => 'spatie.permission.cache',
|
||||||
|
|
||||||
/*
|
|
||||||
* You may optionally indicate a specific cache driver to use for permission and
|
|
||||||
* role caching using any of the `store` drivers listed in the cache.php config
|
|
||||||
* file. Using 'default' here means to use the `default` set in cache.php.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'store' => 'default',
|
'store' => 'default',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,34 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Queue Connection Name
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Laravel's queue supports a variety of backends via a single, unified
|
|
||||||
| API, giving you convenient access to each backend using identical
|
|
||||||
| syntax for each. The default queue connection is defined below.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Queue Connections
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may configure the connection options for every queue backend
|
|
||||||
| used by your application. An example configuration is provided for
|
|
||||||
| each backend supported by Laravel. You're also free to add more.
|
|
||||||
|
|
|
||||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
|
||||||
| "deferred", "background", "failover", "null"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'connections' => [
|
'connections' => [
|
||||||
|
|
||||||
'sync' => [
|
'sync' => [
|
||||||
@ -90,36 +63,10 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Job Batching
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The following options configure the database and table that store job
|
|
||||||
| batching information. These options can be updated to any database
|
|
||||||
| connection and table which has been defined by your application.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'batching' => [
|
'batching' => [
|
||||||
'database' => env('DB_BATCHING_CONNECTION', env('DEMO', false) ? 'pgsql_public' : env('DB_CONNECTION', 'sqlite')),
|
'database' => env('DB_BATCHING_CONNECTION', env('DEMO', false) ? 'pgsql_public' : env('DB_CONNECTION', 'sqlite')),
|
||||||
'table' => 'job_batches',
|
'table' => 'job_batches',
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Failed Queue Jobs
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| These options configure the behavior of failed queue job logging so you
|
|
||||||
| can control how and where failed jobs are stored. Laravel ships with
|
|
||||||
| support for storing failed jobs in a simple file or in a database.
|
|
||||||
|
|
|
||||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'failed' => [
|
'failed' => [
|
||||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||||
'database' => env('DB_FAILED_CONNECTION', env('DEMO', false) ? 'pgsql_public' : env('DB_CONNECTION', 'sqlite')),
|
'database' => env('DB_FAILED_CONNECTION', env('DEMO', false) ? 'pgsql_public' : env('DB_CONNECTION', 'sqlite')),
|
||||||
|
|||||||
@ -1,31 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Reverb Server
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option controls the default server used by Reverb to handle
|
|
||||||
| incoming messages as well as broadcasting message to all your
|
|
||||||
| connected clients. At this time only "reverb" is supported.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'default' => env('REVERB_SERVER', 'reverb'),
|
'default' => env('REVERB_SERVER', 'reverb'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Reverb Servers
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may define details for each of the supported Reverb servers.
|
|
||||||
| Each server has its own configuration options that are defined in
|
|
||||||
| the array below. You should ensure all the options are present.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'servers' => [
|
'servers' => [
|
||||||
|
|
||||||
'reverb' => [
|
'reverb' => [
|
||||||
@ -55,18 +31,6 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Reverb Applications
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may define how Reverb applications are managed. If you choose
|
|
||||||
| to use the "config" provider, you may define an array of apps which
|
|
||||||
| your server will support, including their connection credentials.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'apps' => [
|
'apps' => [
|
||||||
|
|
||||||
'provider' => 'config',
|
'provider' => 'config',
|
||||||
|
|||||||
@ -1,19 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Third Party Services
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This file is for storing the credentials for third party services such
|
|
||||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
|
||||||
| location for this type of information, allowing packages to have
|
|
||||||
| a conventional file to locate the various service credentials.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'postmark' => [
|
'postmark' => [
|
||||||
'key' => env('POSTMARK_API_KEY'),
|
'key' => env('POSTMARK_API_KEY'),
|
||||||
],
|
],
|
||||||
@ -57,4 +44,8 @@ return [
|
|||||||
'enabled' => env('ENABLE_APPLE_LOGIN', false),
|
'enabled' => env('ENABLE_APPLE_LOGIN', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'google_maps' => [
|
||||||
|
'api_key' => env('GOOGLE_MAPS_API_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -3,215 +3,25 @@
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default Session Driver
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option determines the default session driver that is utilized for
|
|
||||||
| incoming requests. Laravel supports a variety of storage options to
|
|
||||||
| persist session data. Database storage is a great default choice.
|
|
||||||
|
|
|
||||||
| Supported: "file", "cookie", "database", "memcached",
|
|
||||||
| "redis", "dynamodb", "array"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'driver' => env('SESSION_DRIVER', 'database'),
|
'driver' => env('SESSION_DRIVER', 'database'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Lifetime
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may specify the number of minutes that you wish the session
|
|
||||||
| to be allowed to remain idle before it expires. If you want them
|
|
||||||
| to expire immediately when the browser is closed then you may
|
|
||||||
| indicate that via the expire_on_close configuration option.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||||
|
|
||||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Encryption
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option allows you to easily specify that all of your session data
|
|
||||||
| should be encrypted before it's stored. All encryption is performed
|
|
||||||
| automatically by Laravel and you may use the session like normal.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session File Location
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When utilizing the "file" session driver, the session files are placed
|
|
||||||
| on disk. The default storage location is defined here; however, you
|
|
||||||
| are free to provide another location where they should be stored.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'files' => storage_path('framework/sessions'),
|
'files' => storage_path('framework/sessions'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Database Connection
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When using the "database" or "redis" session drivers, you may specify a
|
|
||||||
| connection that should be used to manage these sessions. This should
|
|
||||||
| correspond to a connection in your database configuration options.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'connection' => env('SESSION_CONNECTION', env('DEMO', false) ? 'pgsql_public' : null),
|
'connection' => env('SESSION_CONNECTION', env('DEMO', false) ? 'pgsql_public' : null),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Database Table
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When using the "database" session driver, you may specify the table to
|
|
||||||
| be used to store sessions. Of course, a sensible default is defined
|
|
||||||
| for you; however, you're welcome to change this to another table.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'table' => env('SESSION_TABLE', 'sessions'),
|
'table' => env('SESSION_TABLE', 'sessions'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Cache Store
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When using one of the framework's cache driven session backends, you may
|
|
||||||
| define the cache store which should be used to store the session data
|
|
||||||
| between requests. This must match one of your defined cache stores.
|
|
||||||
|
|
|
||||||
| Affects: "dynamodb", "memcached", "redis"
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'store' => env('SESSION_STORE'),
|
'store' => env('SESSION_STORE'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Sweeping Lottery
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Some session drivers must manually sweep their storage location to get
|
|
||||||
| rid of old sessions from storage. Here are the chances that it will
|
|
||||||
| happen on a given request. By default, the odds are 2 out of 100.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'lottery' => [2, 100],
|
'lottery' => [2, 100],
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Cookie Name
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may change the name of the session cookie that is created by
|
|
||||||
| the framework. Typically, you should not need to change this value
|
|
||||||
| since doing so does not grant a meaningful security improvement.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'cookie' => env(
|
'cookie' => env(
|
||||||
'SESSION_COOKIE',
|
'SESSION_COOKIE',
|
||||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||||
),
|
),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Cookie Path
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The session cookie path determines the path for which the cookie will
|
|
||||||
| be regarded as available. Typically, this will be the root path of
|
|
||||||
| your application, but you're free to change this when necessary.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'path' => env('SESSION_PATH', '/'),
|
'path' => env('SESSION_PATH', '/'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Session Cookie Domain
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This value determines the domain and subdomains the session cookie is
|
|
||||||
| available to. By default, the cookie will be available to the root
|
|
||||||
| domain without subdomains. Typically, this shouldn't be changed.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'domain' => env('SESSION_DOMAIN'),
|
'domain' => env('SESSION_DOMAIN'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| HTTPS Only Cookies
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| By setting this option to true, session cookies will only be sent back
|
|
||||||
| to the server if the browser has a HTTPS connection. This will keep
|
|
||||||
| the cookie from being sent to you when it can't be done securely.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| HTTP Access Only
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Setting this value to true will prevent JavaScript from accessing the
|
|
||||||
| value of the cookie and the cookie will only be accessible through
|
|
||||||
| the HTTP protocol. It's unlikely you should disable this option.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Same-Site Cookies
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| This option determines how your cookies behave when cross-site requests
|
|
||||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
|
||||||
| will set this value to "lax" to permit secure cross-site requests.
|
|
||||||
|
|
|
||||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
|
||||||
|
|
|
||||||
| Supported: "lax", "strict", "none", null
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Partitioned Cookies
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Setting this value to true will tie the cookie to the top-level site for
|
|
||||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
|
||||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -6,9 +6,6 @@ use Illuminate\Support\Facades\Schema;
|
|||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('cache', function (Blueprint $table) {
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
@ -24,9 +21,6 @@ return new class extends Migration
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('cache');
|
Schema::dropIfExists('cache');
|
||||||
|
|||||||
@ -6,9 +6,6 @@ use Illuminate\Support\Facades\Schema;
|
|||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('jobs', function (Blueprint $table) {
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
@ -45,9 +42,6 @@ return new class extends Migration
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('jobs');
|
Schema::dropIfExists('jobs');
|
||||||
|
|||||||
@ -6,9 +6,6 @@ use Illuminate\Support\Facades\Schema;
|
|||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$teams = config('permission.teams');
|
$teams = config('permission.teams');
|
||||||
@ -19,10 +16,6 @@ return new class extends Migration
|
|||||||
|
|
||||||
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
|
||||||
/**
|
|
||||||
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
|
||||||
*/
|
|
||||||
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||||
$table->id(); // permission id
|
$table->id(); // permission id
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
@ -31,10 +24,6 @@ return new class extends Migration
|
|||||||
|
|
||||||
$table->unique(['name', 'guard_name']);
|
$table->unique(['name', 'guard_name']);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
|
||||||
*/
|
|
||||||
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||||
$table->id(); // role id
|
$table->id(); // role id
|
||||||
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||||
@ -119,9 +108,6 @@ return new class extends Migration
|
|||||||
->forget(config('permission.cache.key'));
|
->forget(config('permission.cache.key'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
$tableNames = config('permission.table_names');
|
$tableNames = config('permission.table_names');
|
||||||
|
|||||||
@ -6,9 +6,6 @@ use Illuminate\Support\Facades\Schema;
|
|||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||||
@ -23,9 +20,6 @@ return new class extends Migration
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('personal_access_tokens');
|
Schema::dropIfExists('personal_access_tokens');
|
||||||
|
|||||||
@ -10,7 +10,6 @@ class DatabaseSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
$this->call([
|
$this->call([
|
||||||
\Modules\User\Database\Seeders\AuthUserSeeder::class,
|
\Modules\User\Database\Seeders\AuthUserSeeder::class,
|
||||||
HomeSliderSettingsSeeder::class,
|
|
||||||
\Modules\Location\Database\Seeders\LocationSeeder::class,
|
\Modules\Location\Database\Seeders\LocationSeeder::class,
|
||||||
\Modules\Category\Database\Seeders\CategorySeeder::class,
|
\Modules\Category\Database\Seeders\CategorySeeder::class,
|
||||||
\Modules\Listing\Database\Seeders\ListingCustomFieldSeeder::class,
|
\Modules\Listing\Database\Seeders\ListingCustomFieldSeeder::class,
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Database\Seeders;
|
|
||||||
|
|
||||||
use App\Support\HomeSlideDefaults;
|
|
||||||
use App\Settings\GeneralSettings;
|
|
||||||
use Illuminate\Database\Seeder;
|
|
||||||
|
|
||||||
class HomeSliderSettingsSeeder extends Seeder
|
|
||||||
{
|
|
||||||
public function run(): void
|
|
||||||
{
|
|
||||||
$settings = app(GeneralSettings::class);
|
|
||||||
$settings->home_slides = HomeSlideDefaults::defaults();
|
|
||||||
|
|
||||||
$settings->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,13 +8,28 @@ return new class extends SettingsMigration
|
|||||||
{
|
{
|
||||||
$this->migrator->add('general.site_name', 'OpenClassify');
|
$this->migrator->add('general.site_name', 'OpenClassify');
|
||||||
$this->migrator->add('general.site_description', 'The marketplace for buying and selling everything.');
|
$this->migrator->add('general.site_description', 'The marketplace for buying and selling everything.');
|
||||||
|
$this->migrator->add('general.media_disk', \Modules\S3\Support\MediaStorage::defaultDriver());
|
||||||
$this->migrator->add('general.site_logo', null);
|
$this->migrator->add('general.site_logo', null);
|
||||||
|
$this->migrator->add('general.site_logo_disk', null);
|
||||||
$this->migrator->add('general.default_language', 'en');
|
$this->migrator->add('general.default_language', 'en');
|
||||||
|
$this->migrator->add('general.default_country_code', '+90');
|
||||||
$this->migrator->add('general.currencies', ['USD']);
|
$this->migrator->add('general.currencies', ['USD']);
|
||||||
$this->migrator->add('general.sender_email', 'hello@example.com');
|
$this->migrator->add('general.sender_email', 'hello@example.com');
|
||||||
$this->migrator->add('general.sender_name', 'OpenClassify');
|
$this->migrator->add('general.sender_name', 'OpenClassify');
|
||||||
$this->migrator->add('general.linkedin_url', null);
|
$this->migrator->add('general.linkedin_url', null);
|
||||||
$this->migrator->add('general.instagram_url', null);
|
$this->migrator->add('general.instagram_url', null);
|
||||||
$this->migrator->add('general.whatsapp', null);
|
$this->migrator->add('general.whatsapp', null);
|
||||||
|
$this->migrator->add('general.enable_google_maps', false);
|
||||||
|
$this->migrator->add('general.google_maps_api_key', null);
|
||||||
|
$this->migrator->add('general.enable_google_login', false);
|
||||||
|
$this->migrator->add('general.google_client_id', null);
|
||||||
|
$this->migrator->add('general.google_client_secret', null);
|
||||||
|
$this->migrator->add('general.enable_facebook_login', false);
|
||||||
|
$this->migrator->add('general.facebook_client_id', null);
|
||||||
|
$this->migrator->add('general.facebook_client_secret', null);
|
||||||
|
$this->migrator->add('general.enable_apple_login', false);
|
||||||
|
$this->migrator->add('general.apple_client_id', null);
|
||||||
|
$this->migrator->add('general.apple_client_secret', null);
|
||||||
|
$this->migrator->add('general.home_slides', \Modules\Site\App\Support\HomeSlideDefaults::defaults());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user