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;
|
||||
|
||||
use App\Settings\GeneralSettings;
|
||||
use App\Support\CountryCodeManager;
|
||||
use App\Support\HomeSlideDefaults;
|
||||
use BackedEnum;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Pages\SettingsPage;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Schemas\Schema;
|
||||
use Modules\Admin\Support\HomeSlideFormSchema;
|
||||
use Modules\Location\Support\CountryCodeManager;
|
||||
use Modules\S3\Support\MediaStorage;
|
||||
use Modules\Site\App\Settings\GeneralSettings;
|
||||
use Modules\Site\App\Support\HomeSlideDefaults;
|
||||
use Tapp\FilamentCountryCodeField\Forms\Components\CountryCodeSelect;
|
||||
use UnitEnum;
|
||||
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
|
||||
@ -31,9 +31,9 @@ class ManageGeneralSettings extends SettingsPage
|
||||
|
||||
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;
|
||||
|
||||
@ -246,7 +246,7 @@ class ManageGeneralSettings extends SettingsPage
|
||||
'home_slides' => $this->defaultHomeSlides(),
|
||||
'site_logo_disk' => null,
|
||||
'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_country_code' => CountryCodeManager::normalizeCountryCode(config('app.default_country_code', '+90')),
|
||||
'currencies' => $this->normalizeCurrencies(config('app.currencies', ['TRY'])),
|
||||
@ -272,7 +272,7 @@ class ManageGeneralSettings extends SettingsPage
|
||||
->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] : []);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
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 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\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
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 Illuminate\Database\Eloquent\Builder;
|
||||
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\Support\ListingCustomFieldSchemaBuilder;
|
||||
use Modules\Listing\Support\ListingPanelHelper;
|
||||
use Modules\Location\Models\City;
|
||||
use Modules\Location\Models\Country;
|
||||
use Modules\Video\Support\Filament\VideoFormSchema;
|
||||
use Modules\Listing\Support\Filament\AdminListingResourceSchema;
|
||||
use UnitEnum;
|
||||
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
|
||||
|
||||
class ListingResource extends Resource
|
||||
{
|
||||
@ -49,138 +21,12 @@ class ListingResource extends Resource
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->schema([
|
||||
TextInput::make('title')->required()->maxLength(255)->live(onBlur: true)->afterStateUpdated(fn ($state, $set) => $set('slug', \Illuminate\Support\Str::slug($state).'-'.\Illuminate\Support\Str::random(4))),
|
||||
TextInput::make('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(),
|
||||
]);
|
||||
return $schema->schema(AdminListingResourceSchema::form());
|
||||
}
|
||||
|
||||
public static function table(Table $table): 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 $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));
|
||||
return AdminListingResourceSchema::configureTable($table, static::class);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Admin\Filament\Widgets;
|
||||
|
||||
use Filament\Widgets\ChartWidget;
|
||||
@ -8,6 +9,8 @@ class ListingsTrendChart extends ChartWidget
|
||||
{
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
protected int|string|array $columnSpan = 'full';
|
||||
|
||||
protected ?string $heading = 'Listing Creation Trend';
|
||||
|
||||
protected ?string $description = 'Daily listing volume by selected period.';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Admin\Providers;
|
||||
|
||||
use App\Http\Middleware\BootstrapAppData;
|
||||
use A909M\FilamentStateFusion\FilamentStateFusionPlugin;
|
||||
use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin;
|
||||
use Filament\Http\Middleware\Authenticate;
|
||||
@ -21,13 +21,10 @@ use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use Jeffgreco13\FilamentBreezy\BreezyCore;
|
||||
use MWGuerra\FileManager\FileManagerPlugin;
|
||||
use MWGuerra\FileManager\Filament\Pages\FileManager;
|
||||
use Modules\Demo\App\Http\Middleware\ResolveDemoRequest;
|
||||
use Modules\Admin\Filament\Resources\CategoryResource;
|
||||
use Modules\Admin\Filament\Resources\ListingResource;
|
||||
use Modules\Admin\Filament\Resources\LocationResource;
|
||||
use Modules\Admin\Filament\Resources\UserResource;
|
||||
use Modules\Site\App\Http\Middleware\BootstrapAppData;
|
||||
use MWGuerra\FileManager\Filament\Pages\FileManager;
|
||||
use MWGuerra\FileManager\FileManagerPlugin;
|
||||
|
||||
class AdminPanelProvider extends PanelProvider
|
||||
{
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@ -114,6 +114,72 @@ class Category extends Model
|
||||
->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
|
||||
{
|
||||
return static::query()
|
||||
|
||||
@ -107,7 +107,7 @@ class ConversationDemoSeeder extends Seeder
|
||||
$readAfterMinutes = $payload['read_after_minutes'];
|
||||
$readAt = is_numeric($readAfterMinutes) ? $createdAt->copy()->addMinutes((int) $readAfterMinutes) : null;
|
||||
|
||||
$message = new ConversationMessage();
|
||||
$message = new ConversationMessage;
|
||||
$message->forceFill([
|
||||
'conversation_id' => $conversation->getKey(),
|
||||
'sender_id' => $sender->getKey(),
|
||||
@ -5,10 +5,10 @@
|
||||
@section('content')
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||
<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">
|
||||
@include('panel.partials.page-header', [
|
||||
@include('panel::partials.page-header', [
|
||||
'title' => 'Inbox',
|
||||
'description' => 'Read and reply to buyer messages from the same panel shell used across the site.',
|
||||
'actions' => $requiresLogin ?? false
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
|
||||
namespace Modules\Demo\App\Support;
|
||||
|
||||
use App\Settings\GeneralSettings;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\Demo\App\Models\DemoInstance;
|
||||
use Modules\Site\App\Settings\GeneralSettings;
|
||||
use Modules\User\App\Models\User;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
use Throwable;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
@section('content')
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||
<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">
|
||||
@if($requiresLogin ?? false)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@ -30,10 +31,26 @@ return new class extends Migration
|
||||
$table->decimal('longitude', 10, 7)->nullable();
|
||||
$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
|
||||
{
|
||||
Schema::dropIfExists('listing_custom_fields');
|
||||
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
|
||||
|
||||
namespace Modules\Listing\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
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\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Category\Models\Category;
|
||||
use Modules\Listing\Support\ListingImageViewData;
|
||||
use Modules\Listing\States\ListingStatus;
|
||||
use Modules\Listing\Support\ListingImageViewData;
|
||||
use Modules\Listing\Support\ListingPanelHelper;
|
||||
use Modules\User\App\Models\User;
|
||||
use Modules\Video\Enums\VideoStatus;
|
||||
use Modules\Video\Models\Video;
|
||||
use Spatie\Image\Enums\Fit;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Image\Enums\Fit;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
@ -97,7 +100,7 @@ class Listing extends Model implements HasMedia
|
||||
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);
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
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()
|
||||
->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
|
||||
{
|
||||
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
|
||||
{
|
||||
$payload = Arr::only($attributes, [
|
||||
@ -460,7 +552,7 @@ class Listing extends Model implements HasMedia
|
||||
$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 = $baseSlug !== '' ? $baseSlug : 'listing';
|
||||
|
||||
@ -9,10 +9,15 @@ use Modules\Category\Models\Category;
|
||||
class ListingCustomField extends Model
|
||||
{
|
||||
public const TYPE_TEXT = 'text';
|
||||
|
||||
public const TYPE_TEXTAREA = 'textarea';
|
||||
|
||||
public const TYPE_NUMBER = 'number';
|
||||
|
||||
public const TYPE_SELECT = 'select';
|
||||
|
||||
public const TYPE_BOOLEAN = 'boolean';
|
||||
|
||||
public const TYPE_DATE = 'date';
|
||||
|
||||
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\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Illuminate\Support\Carbon;
|
||||
@ -22,9 +22,6 @@ class ListingCustomFieldSchemaBuilder
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Component>
|
||||
*/
|
||||
public static function formComponents(?int $categoryId): array
|
||||
{
|
||||
return ListingCustomField::query()
|
||||
@ -38,10 +35,6 @@ class ListingCustomFieldSchemaBuilder
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $values
|
||||
* @return array<int, array{label: string, value: string}>
|
||||
*/
|
||||
public static function presentableValues(?int $categoryId, array $values): array
|
||||
{
|
||||
if ($values === []) {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Modules\Listing\Support;
|
||||
|
||||
use App\Settings\GeneralSettings;
|
||||
use Modules\Site\App\Settings\GeneralSettings;
|
||||
use Throwable;
|
||||
|
||||
class ListingPanelHelper
|
||||
@ -32,7 +32,7 @@ class ListingPanelHelper
|
||||
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));
|
||||
$codes = self::currencyCodes();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
namespace Modules\Listing\Support;
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
@ -12,16 +12,6 @@ use function Laravel\Ai\agent;
|
||||
|
||||
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
|
||||
{
|
||||
$provider = (string) config('quick-listing.ai_provider', 'openai');
|
||||
@ -39,11 +29,7 @@ class QuickListingCategorySuggester
|
||||
];
|
||||
}
|
||||
|
||||
$categories = Category::query()
|
||||
->where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'parent_id']);
|
||||
$categories = Category::activeAiCatalog();
|
||||
|
||||
if ($categories->isEmpty()) {
|
||||
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
|
||||
{
|
||||
$byId = $categories->keyBy('id');
|
||||
@ -156,4 +138,3 @@ class QuickListingCategorySuggester
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,12 +229,12 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="listing-filter-card px-4 py-3 hidden lg:flex flex-col xl:flex-row xl:items-center gap-3">
|
||||
<p class="text-sm text-slate-700 mr-auto">
|
||||
<div class="listing-results-bar listing-filter-card hidden lg:flex">
|
||||
<p class="listing-results-meta">
|
||||
<strong>{{ number_format($resultListingsCount) }}</strong>
|
||||
{{ $activeCategoryName !== '' ? ' listings found in '.$activeCategoryName : ' listings found' }}
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="listing-results-actions">
|
||||
@auth
|
||||
<form method="POST" action="{{ route('favorites.searches.store') }}">
|
||||
@csrf
|
||||
@ -276,9 +276,9 @@
|
||||
<input type="hidden" name="date_filter" value="{{ $dateFilter }}">
|
||||
@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>
|
||||
<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="newest" @selected($sort === 'newest')>Newest</option>
|
||||
<option value="oldest" @selected($sort === 'oldest')>Oldest</option>
|
||||
@ -570,7 +570,6 @@
|
||||
countrySelect.value = matchedCountryOption.value;
|
||||
await loadCities(matchedCountryOption.value, cityName);
|
||||
} catch (error) {
|
||||
// no-op
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@ -43,9 +43,6 @@ class LocationSeeder extends Seeder
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{code: string, name: string, phone_code: string}>
|
||||
*/
|
||||
private function countries(): array
|
||||
{
|
||||
$countries = [];
|
||||
@ -84,7 +81,7 @@ class LocationSeeder extends Seeder
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = 'filament-country-code-field::countries.' . $value;
|
||||
$key = 'filament-country-code-field::countries.'.$value;
|
||||
$labelEn = trim((string) trans($key, [], 'en'));
|
||||
|
||||
$name = $labelEn !== '' && $labelEn !== $key ? $labelEn : strtoupper($value);
|
||||
@ -112,9 +109,6 @@ class LocationSeeder extends Seeder
|
||||
return substr($normalized, 0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function turkeyCities(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
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\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
|
||||
@ -28,12 +30,12 @@ class City extends Model
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
public function country()
|
||||
public function country(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Country::class);
|
||||
}
|
||||
|
||||
public function districts()
|
||||
public function districts(): HasMany
|
||||
{
|
||||
return $this->hasMany(District::class);
|
||||
}
|
||||
@ -53,4 +55,31 @@ class City extends Model
|
||||
->pluck('name', 'name')
|
||||
->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\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
|
||||
@ -28,7 +29,7 @@ class Country extends Model
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
|
||||
public function cities()
|
||||
public function cities(): HasMany
|
||||
{
|
||||
return $this->hasMany(City::class);
|
||||
}
|
||||
@ -59,4 +60,75 @@ class Country extends Model
|
||||
->pluck('name', 'name')
|
||||
->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
|
||||
|
||||
namespace App\Support;
|
||||
namespace Modules\Location\Support;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Tapp\FilamentCountryCodeField\Enums\CountriesEnum;
|
||||
@ -17,7 +17,7 @@ class CountryCodeManager
|
||||
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);
|
||||
|
||||
@ -32,7 +32,7 @@ class CountryCodeManager
|
||||
return self::countryCodeFromIso2($value) ?? '+90';
|
||||
}
|
||||
|
||||
public static function isValidCountryCode(null | string $value): bool
|
||||
public static function isValidCountryCode(?string $value): bool
|
||||
{
|
||||
if (! filled($value)) {
|
||||
return false;
|
||||
@ -41,7 +41,7 @@ class CountryCodeManager
|
||||
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));
|
||||
|
||||
@ -53,7 +53,7 @@ class CountryCodeManager
|
||||
->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);
|
||||
|
||||
@ -65,7 +65,7 @@ class CountryCodeManager
|
||||
->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);
|
||||
|
||||
@ -77,7 +77,7 @@ class CountryCodeManager
|
||||
->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);
|
||||
|
||||
@ -111,7 +111,7 @@ class CountryCodeManager
|
||||
})['country_code'] ?? null;
|
||||
}
|
||||
|
||||
public static function normalizeStoredCountry(null | string $value): ?string
|
||||
public static function normalizeStoredCountry(?string $value): ?string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
|
||||
@ -128,9 +128,6 @@ class CountryCodeManager
|
||||
return self::labelFromCountryCode($countryCode) ?? $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, array{country_code: string, iso2: string, label: string, english_label: string}>
|
||||
*/
|
||||
private static function countries(): Collection
|
||||
{
|
||||
static $countries;
|
||||
@ -1,50 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Location\Models\Country;
|
||||
use Modules\Location\Http\Controllers\LocationLookupController;
|
||||
|
||||
Route::get('/locations/cities/{country}', function (string $country) {
|
||||
$lookupValue = trim($country);
|
||||
|
||||
if ($lookupValue === '') {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$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');
|
||||
Route::middleware('web')->group(function () {
|
||||
Route::get('/locations/cities/{country}', [LocationLookupController::class, 'cities'])
|
||||
->name('locations.cities');
|
||||
Route::get('/locations/districts/{city}', [LocationLookupController::class, '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
|
||||
|
||||
namespace App\Livewire;
|
||||
namespace Modules\Panel\App\Livewire;
|
||||
|
||||
use App\Support\QuickListingCategorySuggester;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
@ -16,6 +15,7 @@ use Modules\Listing\Models\Listing;
|
||||
use Modules\Listing\Models\ListingCustomField;
|
||||
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
||||
use Modules\Listing\Support\ListingPanelHelper;
|
||||
use Modules\Listing\Support\QuickListingCategorySuggester;
|
||||
use Modules\Location\Models\City;
|
||||
use Modules\Location\Models\Country;
|
||||
use Modules\S3\Support\MediaStorage;
|
||||
@ -28,36 +28,59 @@ class PanelQuickListingForm extends Component
|
||||
use WithFileUploads;
|
||||
|
||||
private const TOTAL_STEPS = 5;
|
||||
|
||||
private const DRAFT_SESSION_KEY = 'panel_quick_listing_draft';
|
||||
|
||||
private const OTHER_CITY_ID = -1;
|
||||
|
||||
public array $photos = [];
|
||||
|
||||
public array $videos = [];
|
||||
|
||||
public array $categories = [];
|
||||
|
||||
public array $countries = [];
|
||||
|
||||
public array $cities = [];
|
||||
|
||||
public array $listingCustomFields = [];
|
||||
|
||||
public array $customFieldValues = [];
|
||||
|
||||
public int $currentStep = 1;
|
||||
|
||||
public string $categorySearch = '';
|
||||
|
||||
public ?int $selectedCategoryId = null;
|
||||
|
||||
public ?int $activeParentCategoryId = null;
|
||||
|
||||
public ?int $detectedCategoryId = null;
|
||||
|
||||
public ?float $detectedConfidence = null;
|
||||
|
||||
public ?string $detectedReason = null;
|
||||
|
||||
public ?string $detectedError = null;
|
||||
|
||||
public array $detectedAlternatives = [];
|
||||
|
||||
public bool $isDetecting = false;
|
||||
|
||||
public string $listingTitle = '';
|
||||
|
||||
public string $price = '';
|
||||
|
||||
public string $description = '';
|
||||
|
||||
public ?int $selectedCountryId = null;
|
||||
|
||||
public ?int $selectedCityId = null;
|
||||
|
||||
public bool $isPublishing = false;
|
||||
|
||||
public bool $shouldPersistDraft = true;
|
||||
|
||||
public ?string $publishError = null;
|
||||
|
||||
public function mount(): void
|
||||
@ -70,7 +93,7 @@ class PanelQuickListingForm extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('panel.quick-create');
|
||||
return view('panel::quick-create');
|
||||
}
|
||||
|
||||
public function dehydrate(): void
|
||||
@ -596,10 +619,6 @@ class PanelQuickListingForm extends Component
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$profilePhone = Profile::query()
|
||||
->where('user_id', $user->getKey())
|
||||
->value('phone');
|
||||
|
||||
$payload = [
|
||||
'title' => trim($this->listingTitle),
|
||||
'description' => trim($this->description),
|
||||
@ -609,7 +628,7 @@ class PanelQuickListingForm extends Component
|
||||
'status' => 'pending',
|
||||
'custom_fields' => $this->sanitizedCustomFieldValues(),
|
||||
'contact_email' => (string) $user->email,
|
||||
'contact_phone' => $profilePhone,
|
||||
'contact_phone' => Profile::phoneForUser($user),
|
||||
'country' => $this->selectedCountryName,
|
||||
'city' => $this->selectedCityName,
|
||||
];
|
||||
@ -674,75 +693,18 @@ class PanelQuickListingForm extends Component
|
||||
|
||||
private function loadCategories(): void
|
||||
{
|
||||
$all = Category::query()
|
||||
->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();
|
||||
$this->categories = Category::panelQuickCatalog();
|
||||
}
|
||||
|
||||
private function loadLocations(): void
|
||||
{
|
||||
$this->countries = Country::query()
|
||||
->where('is_active', true)
|
||||
->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();
|
||||
$this->countries = Country::quickCreateOptions();
|
||||
$this->cities = City::quickCreateOptions();
|
||||
}
|
||||
|
||||
private function loadListingCustomFields(): void
|
||||
{
|
||||
$this->listingCustomFields = ListingCustomField::query()
|
||||
->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();
|
||||
$this->listingCustomFields = ListingCustomField::panelFieldDefinitions($this->selectedCategoryId);
|
||||
|
||||
$allowed = collect($this->listingCustomFields)->pluck('name')->all();
|
||||
$this->customFieldValues = collect($this->customFieldValues)->only($allowed)->all();
|
||||
@ -762,7 +724,7 @@ class PanelQuickListingForm extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$profile = Profile::query()->where('user_id', $user->getKey())->first();
|
||||
$profile = Profile::detailsForUser($user);
|
||||
|
||||
if (! $profile) {
|
||||
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')
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||
<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">
|
||||
<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="grid gap-6 xl:grid-cols-[300px,minmax(0,1fr)]">
|
||||
<aside class="listings-dashboard-sidebar space-y-6">
|
||||
@include('panel.partials.sidebar', ['activeMenu' => 'listings'])
|
||||
@include('panel::partials.sidebar', ['activeMenu' => 'listings'])
|
||||
</aside>
|
||||
|
||||
<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)]">
|
||||
<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>
|
||||
|
||||
@include('panel.partials.sidebar', ['activeMenu' => 'profile'])
|
||||
@include('panel::partials.sidebar', ['activeMenu' => 'profile'])
|
||||
</aside>
|
||||
|
||||
<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')
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||
<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">
|
||||
<div class="panel-surface p-6">
|
||||
@ -5,10 +5,10 @@
|
||||
@section('content')
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||
<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">
|
||||
@include('panel.partials.page-header', [
|
||||
@include('panel::partials.page-header', [
|
||||
'title' => 'Videos',
|
||||
'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;
|
||||
|
||||
use App\Settings\GeneralSettings;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Site\App\Settings\GeneralSettings;
|
||||
use Throwable;
|
||||
|
||||
final class MediaStorage
|
||||
@ -24,8 +24,6 @@ final class MediaStorage
|
||||
public static function defaultDriver(): string
|
||||
{
|
||||
return self::coerceDriver(config('media_storage.default_driver'))
|
||||
?? self::coerceDriver(env('MEDIA_DISK'))
|
||||
?? self::coerceDriver(env('FILESYSTEM_DISK'))
|
||||
?? self::DRIVER_S3;
|
||||
}
|
||||
|
||||
@ -104,7 +102,7 @@ final class MediaStorage
|
||||
|
||||
config([
|
||||
'filesystems.default' => $disk,
|
||||
'filemanager.disk' => env('FILEMANAGER_DISK', $disk),
|
||||
'filemanager.disk' => $disk,
|
||||
'filament.default_filesystem_disk' => $disk,
|
||||
'media-library.disk_name' => $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
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
namespace Modules\Site\App\Http\Middleware;
|
||||
|
||||
use App\Support\RequestAppData;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Site\App\Support\RequestAppData;
|
||||
|
||||
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();
|
||||
|
||||
@ -1,19 +1,23 @@
|
||||
<?php
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
namespace Modules\Site\App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SetLocale
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
public function handle(Request $request, Closure $next): mixed
|
||||
{
|
||||
$locale = session('locale', config('app.locale'));
|
||||
$available = config('app.available_locales', ['en']);
|
||||
if (!in_array($locale, $available)) {
|
||||
|
||||
if (! in_array($locale, $available, true)) {
|
||||
$locale = config('app.locale');
|
||||
}
|
||||
|
||||
app()->setLocale($locale);
|
||||
|
||||
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
|
||||
|
||||
namespace App\Settings;
|
||||
namespace Modules\Site\App\Settings;
|
||||
|
||||
use Spatie\LaravelSettings\Settings;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
namespace Modules\Site\App\Support;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Modules\S3\Support\MediaStorage;
|
||||
@ -1,14 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
namespace Modules\Site\App\Support;
|
||||
|
||||
use App\Settings\GeneralSettings;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Modules\Category\Models\Category;
|
||||
use Modules\Location\Models\Country;
|
||||
use Modules\Location\Support\CountryCodeManager;
|
||||
use Modules\S3\Support\MediaStorage;
|
||||
use Modules\Site\App\Settings\GeneralSettings;
|
||||
use Modules\User\App\Models\User;
|
||||
use Throwable;
|
||||
|
||||
@ -33,14 +34,14 @@ final class RequestAppData
|
||||
$fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD']));
|
||||
$fallbackDescription = 'Buy and sell everything in your area.';
|
||||
$fallbackHomeSlides = HomeSlideDefaults::defaults();
|
||||
$fallbackGoogleMapsApiKey = env('GOOGLE_MAPS_API_KEY');
|
||||
$fallbackGoogleClientId = env('GOOGLE_CLIENT_ID');
|
||||
$fallbackGoogleClientSecret = env('GOOGLE_CLIENT_SECRET');
|
||||
$fallbackFacebookClientId = env('FACEBOOK_CLIENT_ID');
|
||||
$fallbackFacebookClientSecret = env('FACEBOOK_CLIENT_SECRET');
|
||||
$fallbackAppleClientId = env('APPLE_CLIENT_ID');
|
||||
$fallbackAppleClientSecret = env('APPLE_CLIENT_SECRET');
|
||||
$fallbackDefaultCountryCode = '+90';
|
||||
$fallbackGoogleMapsApiKey = config('services.google_maps.api_key');
|
||||
$fallbackGoogleClientId = config('services.google.client_id');
|
||||
$fallbackGoogleClientSecret = config('services.google.client_secret');
|
||||
$fallbackFacebookClientId = config('services.facebook.client_id');
|
||||
$fallbackFacebookClientSecret = config('services.facebook.client_secret');
|
||||
$fallbackAppleClientId = config('services.apple.client_id');
|
||||
$fallbackAppleClientSecret = config('services.apple.client_secret');
|
||||
$fallbackDefaultCountryCode = (string) config('app.default_country_code', '+90');
|
||||
$fallbackMediaDriver = MediaStorage::defaultDriver();
|
||||
|
||||
$generalSettings = [
|
||||
@ -59,13 +60,13 @@ final class RequestAppData
|
||||
'whatsapp' => null,
|
||||
'google_maps_enabled' => false,
|
||||
'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_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_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_secret' => $fallbackAppleClientSecret,
|
||||
];
|
||||
@ -174,17 +175,7 @@ final class RequestAppData
|
||||
return [];
|
||||
}
|
||||
|
||||
return Country::query()
|
||||
->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();
|
||||
return Country::headerLocationOptions();
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
@ -197,20 +188,7 @@ final class RequestAppData
|
||||
return [];
|
||||
}
|
||||
|
||||
return Category::query()
|
||||
->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();
|
||||
return Category::headerNavigationItems();
|
||||
} catch (Throwable) {
|
||||
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);
|
||||
}
|
||||
|
||||
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\HasAvatar;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@ -29,9 +30,9 @@ class User extends Authenticatable implements FilamentUser, HasAvatar
|
||||
use HasApiTokens;
|
||||
use HasFactory;
|
||||
use HasRoles;
|
||||
use HasStates;
|
||||
use LogsActivity;
|
||||
use Notifiable;
|
||||
use HasStates;
|
||||
use TwoFactorAuthenticatable;
|
||||
|
||||
protected $fillable = ['name', 'email', 'password', 'avatar_url', 'status'];
|
||||
@ -219,4 +220,34 @@ class User extends Authenticatable implements FilamentUser, HasAvatar
|
||||
'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;
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ class Video extends Model
|
||||
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);
|
||||
}
|
||||
@ -143,6 +143,19 @@ class Video extends Model
|
||||
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
|
||||
{
|
||||
if (blank($this->upload_path)) {
|
||||
@ -302,6 +315,11 @@ class Video extends Model
|
||||
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
|
||||
{
|
||||
$this->forceFill([
|
||||
|
||||
@ -2,7 +2,4 @@
|
||||
|
||||
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
|
||||
|
||||
use Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Contracts\Auth\Middleware\AuthenticatesRequests;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
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__))
|
||||
->withRouting(
|
||||
@ -17,13 +19,13 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->web(append: [
|
||||
ResolveDemoRequest::class,
|
||||
\App\Http\Middleware\BootstrapAppData::class,
|
||||
\App\Http\Middleware\SetLocale::class,
|
||||
BootstrapAppData::class,
|
||||
SetLocale::class,
|
||||
]);
|
||||
|
||||
$middleware->appendToPriorityList(StartSession::class, ResolveDemoRequest::class);
|
||||
$middleware->appendToPriorityList(ResolveDemoRequest::class, \App\Http\Middleware\BootstrapAppData::class);
|
||||
$middleware->appendToPriorityList(\App\Http\Middleware\BootstrapAppData::class, \App\Http\Middleware\SetLocale::class);
|
||||
$middleware->appendToPriorityList(ResolveDemoRequest::class, BootstrapAppData::class);
|
||||
$middleware->appendToPriorityList(BootstrapAppData::class, SetLocale::class);
|
||||
$middleware->prependToPriorityList(AuthenticatesRequests::class, ResolveDemoRequest::class);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
|
||||
@ -1,83 +1,11 @@
|
||||
<?php
|
||||
|
||||
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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
|
||||
'available_locales' => ['en', 'tr', 'ar', 'zh', 'es', 'fr', 'de', 'pt', 'ru', 'ja'],
|
||||
@ -87,18 +15,6 @@ return [
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'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',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
@ -108,20 +24,6 @@ return [
|
||||
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' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
|
||||
@ -1,95 +1,22 @@
|
||||
<?php
|
||||
|
||||
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' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'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' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'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' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'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' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
@ -98,18 +25,6 @@ return [
|
||||
'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),
|
||||
|
||||
];
|
||||
|
||||
@ -1,33 +1,7 @@
|
||||
<?php
|
||||
|
||||
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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
|
||||
'reverb' => [
|
||||
@ -42,7 +16,6 @@ return [
|
||||
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
||||
],
|
||||
'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',
|
||||
],
|
||||
'client_options' => [
|
||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@ -3,35 +3,7 @@
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
|
||||
'array' => [
|
||||
@ -61,7 +33,6 @@ return [
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'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-'),
|
||||
|
||||
];
|
||||
|
||||
@ -3,32 +3,7 @@
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
|
||||
'sqlite' => [
|
||||
@ -124,39 +99,13 @@ return [
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'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' => [
|
||||
'table' => 'migrations',
|
||||
'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' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
@ -1,122 +1,23 @@
|
||||
<?php
|
||||
|
||||
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'
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
'disk' => env('FILEMANAGER_DISK', env('FILESYSTEM_DISK', 'public')),
|
||||
'root' => env('FILEMANAGER_ROOT', ''),
|
||||
'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),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
// 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 expiration in minutes (for signed URLs and S3 temporary URLs)
|
||||
'url_expiration' => env('FILEMANAGER_URL_EXPIRATION', 60),
|
||||
|
||||
// Route prefix for streaming endpoints
|
||||
'route_prefix' => env('FILEMANAGER_ROUTE_PREFIX', 'filemanager'),
|
||||
|
||||
// Middleware applied to streaming routes
|
||||
'middleware' => ['web'],
|
||||
|
||||
// Disks that should always use signed routes (even if public)
|
||||
// Useful if you want extra security for certain 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'],
|
||||
|
||||
// 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' => [],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| File Manager Page (Database Mode)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure the File Manager page which uses database mode to track
|
||||
| files with metadata, hierarchy, and relationships.
|
||||
|
|
||||
*/
|
||||
'file_manager' => [
|
||||
'enabled' => true,
|
||||
'navigation' => [
|
||||
@ -126,16 +27,6 @@ return [
|
||||
'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' => [
|
||||
'enabled' => true,
|
||||
'navigation' => [
|
||||
@ -145,46 +36,16 @@ return [
|
||||
'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' => [
|
||||
'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' => [
|
||||
'disk' => env('FILEMANAGER_DISK', env('FILESYSTEM_DISK', 'public')),
|
||||
'directory' => env('FILEMANAGER_UPLOAD_DIR', 'uploads'),
|
||||
'max_file_size' => 100 * 1024, // 100 MB in kilobytes
|
||||
'allowed_mimes' => [
|
||||
// Videos
|
||||
'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',
|
||||
// Documents
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
@ -193,52 +54,25 @@ return [
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'text/plain',
|
||||
// Audio
|
||||
'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/flac',
|
||||
// Archives
|
||||
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Security Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure security settings to prevent malicious file uploads and access.
|
||||
|
|
||||
*/
|
||||
'security' => [
|
||||
// Dangerous extensions that should NEVER be uploaded (executable files)
|
||||
'blocked_extensions' => [
|
||||
// Server-side scripts
|
||||
'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar',
|
||||
'pl', 'py', 'pyc', 'pyo', 'rb', 'sh', 'bash', 'zsh', 'cgi',
|
||||
'asp', 'aspx', 'jsp', 'jspx', 'cfm', 'cfc',
|
||||
// Executables
|
||||
'exe', 'msi', 'dll', 'com', 'bat', 'cmd', 'vbs', 'vbe',
|
||||
'js', 'jse', 'ws', 'wsf', 'wsc', 'wsh', 'ps1', 'psm1',
|
||||
// Other dangerous
|
||||
'htaccess', 'htpasswd', 'ini', 'log', 'sql', 'env',
|
||||
'pem', 'key', 'crt', 'cer',
|
||||
],
|
||||
|
||||
// Files that can contain embedded scripts (XSS risk when served inline)
|
||||
'sanitize_extensions' => ['svg', 'html', 'htm', 'xml'],
|
||||
|
||||
// Validate MIME type matches extension (prevents spoofing)
|
||||
'validate_mime' => true,
|
||||
|
||||
// Rename files to prevent execution (adds random prefix)
|
||||
'rename_uploads' => false,
|
||||
|
||||
// Strip potentially dangerous characters from filenames
|
||||
'sanitize_filenames' => true,
|
||||
|
||||
// Maximum filename length
|
||||
'max_filename_length' => 255,
|
||||
|
||||
// Patterns blocked in filenames (regex)
|
||||
'blocked_filename_patterns' => [
|
||||
'/\.{2,}/', // Multiple dots (path traversal)
|
||||
'/^\./', // Hidden files
|
||||
@ -246,27 +80,8 @@ return [
|
||||
'/[<>:"|?*]/', // 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' => [
|
||||
// Enable/disable authorization checks (set to false during development)
|
||||
'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' => [
|
||||
'view_any' => null, // Access file manager page
|
||||
'view' => null, // View/preview files
|
||||
@ -276,55 +91,15 @@ return [
|
||||
'delete_any' => null, // Bulk delete
|
||||
'download' => null, // Download files
|
||||
],
|
||||
|
||||
// The policy class to use (can be overridden with custom implementation)
|
||||
'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' => [
|
||||
'enabled' => true,
|
||||
'root_label' => env('FILEMANAGER_SIDEBAR_ROOT_LABEL', 'Root'),
|
||||
'heading' => env('FILEMANAGER_SIDEBAR_HEADING', 'Folders'),
|
||||
'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' => [
|
||||
// Built-in types (set to false to disable)
|
||||
'video' => true,
|
||||
'image' => true,
|
||||
'audio' => true,
|
||||
@ -332,10 +107,7 @@ return [
|
||||
'text' => true,
|
||||
'document' => true,
|
||||
'archive' => true,
|
||||
|
||||
// Custom file types (fully-qualified class names)
|
||||
'custom' => [
|
||||
// \App\FileTypes\ThreeDModelFileType::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@ -1,33 +1,7 @@
|
||||
<?php
|
||||
|
||||
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')),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
|
||||
'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' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
@ -6,50 +6,11 @@ use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'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' => [
|
||||
|
||||
'stack' => [
|
||||
|
||||
@ -1,40 +1,7 @@
|
||||
<?php
|
||||
|
||||
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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
|
||||
'smtp' => [
|
||||
@ -55,10 +22,6 @@ return [
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'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' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
|
||||
@ -4,25 +4,7 @@ use Nwidart\Modules\Activators\FileActivator;
|
||||
use Nwidart\Modules\Providers\ConsoleServiceProvider;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Module Namespace
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Default module namespace.
|
||||
|
|
||||
*/
|
||||
'namespace' => 'Modules',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Module Stubs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Default module stubs.
|
||||
|
|
||||
*/
|
||||
'stubs' => [
|
||||
'enabled' => false,
|
||||
'path' => base_path('vendor/nwidart/laravel-modules/src/Commands/stubs'),
|
||||
@ -60,57 +42,11 @@ return [
|
||||
'gitkeep' => true,
|
||||
],
|
||||
'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 assets path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may update the modules' assets path.
|
||||
|
|
||||
*/
|
||||
'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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| The app path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| app folder name
|
||||
| for example can change it to 'src' or 'App'
|
||||
*/
|
||||
'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' => [
|
||||
// app/
|
||||
'actions' => ['path' => 'app/Actions', 'generate' => false],
|
||||
'casts' => ['path' => 'app/Casts', 'generate' => false],
|
||||
'channels' => ['path' => 'app/Broadcasting', 'generate' => false],
|
||||
@ -137,107 +73,36 @@ return [
|
||||
'services' => ['path' => 'app/Services', 'generate' => false],
|
||||
'scopes' => ['path' => 'app/Models/Scopes', 'generate' => false],
|
||||
'traits' => ['path' => 'app/Traits', 'generate' => false],
|
||||
|
||||
// app/Http/
|
||||
'controller' => ['path' => 'app/Http/Controllers', 'generate' => true],
|
||||
'filter' => ['path' => 'app/Http/Middleware', 'generate' => false],
|
||||
'request' => ['path' => 'app/Http/Requests', 'generate' => false],
|
||||
|
||||
// config/
|
||||
'config' => ['path' => 'config', 'generate' => true],
|
||||
|
||||
// Database/
|
||||
'factory' => ['path' => 'Database/Factories', 'generate' => true],
|
||||
'migration' => ['path' => 'Database/migrations', 'generate' => true],
|
||||
'seeder' => ['path' => 'Database/Seeders', 'generate' => true],
|
||||
|
||||
// lang/
|
||||
'lang' => ['path' => 'lang', 'generate' => false],
|
||||
|
||||
// resource/
|
||||
'assets' => ['path' => 'resources/assets', 'generate' => true],
|
||||
'component-view' => ['path' => 'resources/views/components', 'generate' => false],
|
||||
'views' => ['path' => 'resources/views', 'generate' => true],
|
||||
|
||||
// routes/
|
||||
'routes' => ['path' => 'routes', 'generate' => true],
|
||||
|
||||
// tests/
|
||||
'test-feature' => ['path' => 'tests/Feature', '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' => [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migrations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option for register migration automatically.
|
||||
|
|
||||
*/
|
||||
'migrations' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Translations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option for register lang file automatically.
|
||||
|
|
||||
*/
|
||||
'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()
|
||||
->merge([
|
||||
// New commands go here
|
||||
])->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' => [
|
||||
'enabled' => false,
|
||||
'paths' => [
|
||||
base_path('vendor/*/*'),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Composer File Template
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is the config for the composer.json file, generated by this package
|
||||
|
|
||||
*/
|
||||
'composer' => [
|
||||
'vendor' => env('MODULE_VENDOR', 'nwidart'),
|
||||
'author' => [
|
||||
@ -246,31 +111,10 @@ return [
|
||||
],
|
||||
'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' => [
|
||||
'translations' => true,
|
||||
/**
|
||||
* load files on boot or register method
|
||||
*/
|
||||
'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' => [
|
||||
'file' => [
|
||||
'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 [
|
||||
|
||||
'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,
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
],
|
||||
|
||||
'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',
|
||||
|
||||
/*
|
||||
* 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',
|
||||
|
||||
/*
|
||||
* 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',
|
||||
|
||||
/*
|
||||
* 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',
|
||||
|
||||
/*
|
||||
* 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',
|
||||
],
|
||||
|
||||
'column_names' => [
|
||||
/*
|
||||
* Change this if you want to name the related pivots other than defaults
|
||||
*/
|
||||
'role_pivot_key' => null, // default 'role_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',
|
||||
|
||||
/*
|
||||
* 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',
|
||||
],
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
/*
|
||||
* The class to use to resolve the permissions team id
|
||||
*/
|
||||
'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,
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
/*
|
||||
* 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,
|
||||
|
||||
/*
|
||||
* By default wildcard permission lookups are disabled.
|
||||
* See documentation to understand supported syntax.
|
||||
*/
|
||||
|
||||
'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' => [
|
||||
|
||||
/*
|
||||
* 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'),
|
||||
|
||||
/*
|
||||
* The cache key used to store all permissions.
|
||||
*/
|
||||
|
||||
'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',
|
||||
],
|
||||
];
|
||||
|
||||
@ -1,34 +1,7 @@
|
||||
<?php
|
||||
|
||||
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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
|
||||
'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' => [
|
||||
'database' => env('DB_BATCHING_CONNECTION', env('DEMO', false) ? 'pgsql_public' : env('DB_CONNECTION', 'sqlite')),
|
||||
'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' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_FAILED_CONNECTION', env('DEMO', false) ? 'pgsql_public' : env('DB_CONNECTION', 'sqlite')),
|
||||
|
||||
@ -1,31 +1,7 @@
|
||||
<?php
|
||||
|
||||
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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
|
||||
'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' => [
|
||||
|
||||
'provider' => 'config',
|
||||
|
||||
@ -1,19 +1,6 @@
|
||||
<?php
|
||||
|
||||
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' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
@ -57,4 +44,8 @@ return [
|
||||
'enabled' => env('ENABLE_APPLE_LOGIN', false),
|
||||
],
|
||||
|
||||
'google_maps' => [
|
||||
'api_key' => env('GOOGLE_MAPS_API_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@ -3,215 +3,25 @@
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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),
|
||||
|
||||
'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),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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(
|
||||
'SESSION_COOKIE',
|
||||
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', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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),
|
||||
|
||||
];
|
||||
|
||||
@ -6,9 +6,6 @@ use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
@ -24,9 +21,6 @@ return new class extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
|
||||
@ -6,9 +6,6 @@ use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
@ -45,9 +42,6 @@ return new class extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
|
||||
@ -6,9 +6,6 @@ use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
$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($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) {
|
||||
$table->id(); // permission id
|
||||
$table->string('name');
|
||||
@ -31,10 +24,6 @@ return new class extends Migration
|
||||
|
||||
$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) {
|
||||
$table->id(); // role id
|
||||
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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
$tableNames = config('permission.table_names');
|
||||
|
||||
@ -6,9 +6,6 @@ use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||
@ -23,9 +20,6 @@ return new class extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('personal_access_tokens');
|
||||
|
||||
@ -10,7 +10,6 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
$this->call([
|
||||
\Modules\User\Database\Seeders\AuthUserSeeder::class,
|
||||
HomeSliderSettingsSeeder::class,
|
||||
\Modules\Location\Database\Seeders\LocationSeeder::class,
|
||||
\Modules\Category\Database\Seeders\CategorySeeder::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_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_disk', null);
|
||||
$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.sender_email', 'hello@example.com');
|
||||
$this->migrator->add('general.sender_name', 'OpenClassify');
|
||||
$this->migrator->add('general.linkedin_url', null);
|
||||
$this->migrator->add('general.instagram_url', 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