This commit is contained in:
fatihalp 2026-03-03 13:59:46 +03:00
parent 024a21306c
commit c1500cd8e5
29 changed files with 3662 additions and 109 deletions

View File

@ -2,6 +2,7 @@
namespace Modules\Admin\Filament\Pages;
use App\Support\CountryCodeManager;
use App\Settings\GeneralSettings;
use BackedEnum;
use Filament\Forms\Components\FileUpload;
@ -12,6 +13,7 @@ use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Pages\SettingsPage;
use Filament\Schemas\Schema;
use Tapp\FilamentCountryCodeField\Forms\Components\CountryCodeSelect;
use UnitEnum;
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
@ -61,6 +63,11 @@ class ManageGeneralSettings extends SettingsPage
->options($this->localeOptions())
->required()
->searchable(),
CountryCodeSelect::make('default_country_code')
->label('Default Country')
->default('+90')
->required()
->helperText('Used as default country in panel forms.'),
TagsInput::make('currencies')
->label('Currencies')
->placeholder('USD')
@ -81,7 +88,7 @@ class ManageGeneralSettings extends SettingsPage
->maxLength(255),
PhoneInput::make('whatsapp')
->label('WhatsApp')
->defaultCountry('TR')
->defaultCountry(CountryCodeManager::defaultCountryIso2())
->nullable()
->formatAsYouType()
->helperText('Use international format, e.g. +905551112233.'),

View File

@ -4,7 +4,7 @@ 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\Settings\GeneralSettings;
use App\Support\CountryCodeManager;
use BackedEnum;
use Cheesegrits\FilamentGoogleMaps\Fields\Map;
use Filament\Actions\Action;
@ -24,7 +24,8 @@ use Filament\Tables\Table;
use Modules\Admin\Filament\Resources\ListingResource\Pages;
use Modules\Category\Models\Category;
use Modules\Listing\Models\Listing;
use Throwable;
use Modules\Listing\Support\ListingPanelHelper;
use Tapp\FilamentCountryCodeField\Forms\Components\CountryCodeSelect;
use UnitEnum;
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
@ -40,28 +41,33 @@ class ListingResource extends Resource
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()->prefix('$'),
TextInput::make('price')
->numeric()
->currencyMask(thousandSeparator: ',', decimalSeparator: '.', precision: 2),
Select::make('currency')
->options(fn () => self::currencyOptions())
->default(fn () => self::defaultCurrency())
->options(fn () => ListingPanelHelper::currencyOptions())
->default(fn () => ListingPanelHelper::defaultCurrency())
->required(),
Select::make('category_id')->label('Category')->options(fn () => Category::where('is_active', true)->pluck('name', 'id'))->searchable()->nullable(),
StateFusionSelect::make('status')->required(),
PhoneInput::make('contact_phone')->defaultCountry('TR')->nullable(),
PhoneInput::make('contact_phone')->defaultCountry(CountryCodeManager::defaultCountryIso2())->nullable(),
TextInput::make('contact_email')->email()->maxLength(255),
Toggle::make('is_featured')->default(false),
TextInput::make('city')->maxLength(100),
TextInput::make('country')->maxLength(100),
CountryCodeSelect::make('country')
->label('Country')
->default(fn () => CountryCodeManager::defaultCountryCode())
->formatStateUsing(fn ($state): ?string => CountryCodeManager::countryCodeFromLabelOrCode($state))
->dehydrateStateUsing(fn ($state, ?Listing $record): ?string => CountryCodeManager::normalizeStoredCountry($state ?? $record?->country)),
Map::make('location')
->label('Location')
->visible(fn (): bool => self::googleMapsEnabled())
->visible(fn (): bool => ListingPanelHelper::googleMapsEnabled())
->draggable()
->clickable()
->autocomplete('city')
->autocompleteReverse(true)
->reverseGeocode([
'city' => '%L',
'country' => '%C',
])
->defaultLocation([41.0082, 28.9784])
->defaultZoom(10)
@ -84,7 +90,9 @@ class ListingResource extends Resource
TextColumn::make('id')->sortable(),
TextColumn::make('title')->searchable()->sortable()->limit(40),
TextColumn::make('category.name')->label('Category'),
TextColumn::make('price')->money('USD')->sortable(),
TextColumn::make('price')
->currency(fn (Listing $record): string => $record->currency ?: ListingPanelHelper::defaultCurrency())
->sortable(),
StateFusionSelectColumn::make('status'),
IconColumn::make('is_featured')->boolean()->label('Featured'),
TextColumn::make('city'),
@ -109,35 +117,4 @@ class ListingResource extends Resource
'edit' => Pages\EditListing::route('/{record}/edit'),
];
}
private static function currencyOptions(): array
{
$codes = collect(config('app.currencies', ['USD']))
->filter(fn ($code) => is_string($code) && trim($code) !== '')
->map(fn (string $code) => strtoupper(substr(trim($code), 0, 3)))
->filter(fn (string $code) => strlen($code) === 3)
->unique()
->values()
->all();
if ($codes === []) {
$codes = ['USD'];
}
return collect($codes)->mapWithKeys(fn (string $code) => [$code => $code])->all();
}
private static function defaultCurrency(): string
{
return array_key_first(self::currencyOptions()) ?? 'USD';
}
private static function googleMapsEnabled(): bool
{
try {
return (bool) app(GeneralSettings::class)->enable_google_maps;
} catch (Throwable) {
return false;
}
}
}

View File

@ -24,6 +24,7 @@ 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 TallCms\Cms\TallCmsPlugin;
class AdminPanelProvider extends PanelProvider
{
@ -40,6 +41,8 @@ class AdminPanelProvider extends PanelProvider
->discoverWidgets(in: module_path('Admin', 'Filament/Widgets'), for: 'Modules\\Admin\\Filament\\Widgets')
->plugins([
FilamentStateFusionPlugin::make(),
TallCmsPlugin::make()
->withoutUsers(),
BreezyCore::make()
->myProfile(
shouldRegisterNavigation: true,

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Modules\Category\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use Modules\Category\Models\Category;
use Illuminate\Auth\Access\HandlesAuthorization;
class CategoryPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:Category');
}
public function view(AuthUser $authUser, Category $category): bool
{
return $authUser->can('View:Category');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:Category');
}
public function update(AuthUser $authUser, Category $category): bool
{
return $authUser->can('Update:Category');
}
public function delete(AuthUser $authUser, Category $category): bool
{
return $authUser->can('Delete:Category');
}
public function restore(AuthUser $authUser, Category $category): bool
{
return $authUser->can('Restore:Category');
}
public function forceDelete(AuthUser $authUser, Category $category): bool
{
return $authUser->can('ForceDelete:Category');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:Category');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:Category');
}
public function replicate(AuthUser $authUser, Category $category): bool
{
return $authUser->can('Replicate:Category');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:Category');
}
}

View File

@ -1,18 +1,18 @@
<?php
namespace Modules\Listing\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Modules\Listing\Models\Listing;
use Modules\Listing\Support\ListingPanelHelper;
class ListingController extends Controller
{
public function index(Request $request)
public function index()
{
$listings = Listing::where('status', 'active')
->orderByDesc('is_featured')
->orderByDesc('created_at')
$listings = Listing::query()
->publicFeed()
->paginate(12);
return view('listing::index', compact('listings'));
}
@ -25,13 +25,13 @@ class ListingController extends Controller
public function create()
{
return view('listing::create', [
'currencies' => $this->currencyCodes(),
'currencies' => ListingPanelHelper::currencyCodes(),
]);
}
public function store(Request $request)
{
$currencies = $this->currencyCodes();
$currencies = ListingPanelHelper::currencyCodes();
$data = $request->validate([
'title' => 'required|string|min:3|max:255',
@ -44,23 +44,9 @@ class ListingController extends Controller
'contact_email' => 'nullable|email',
'contact_phone' => 'nullable|string',
]);
$data['user_id'] = auth()->id();
$data['currency'] = strtoupper($data['currency'] ?? $currencies[0]);
$data['slug'] = \Illuminate\Support\Str::slug($data['title']) . '-' . \Illuminate\Support\Str::random(6);
$listing = Listing::create($data);
$listing = Listing::createFromFrontend($data, auth()->id());
return redirect()->route('listings.show', $listing)->with('success', 'Listing created!');
}
private function currencyCodes(): array
{
$codes = collect(config('app.currencies', ['USD']))
->filter(fn ($code) => is_string($code) && trim($code) !== '')
->map(fn (string $code) => strtoupper(substr(trim($code), 0, 3)))
->filter(fn (string $code) => strlen($code) === 3)
->unique()
->values()
->all();
return $codes !== [] ? $codes : ['USD'];
}
}

View File

@ -2,9 +2,12 @@
namespace Modules\Listing\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use Modules\Listing\States\ListingStatus;
use Modules\Listing\Support\ListingPanelHelper;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\MediaLibrary\HasMedia;
@ -52,6 +55,31 @@ class Listing extends Model implements HasMedia
return $this->belongsTo(\App\Models\User::class);
}
public function scopePublicFeed(Builder $query): Builder
{
return $query
->where('status', 'active')
->orderByDesc('is_featured')
->orderByDesc('created_at');
}
public static function createFromFrontend(array $data, null | int | string $userId): self
{
$baseSlug = Str::slug((string) ($data['title'] ?? 'listing'));
$baseSlug = $baseSlug !== '' ? $baseSlug : 'listing';
do {
$slug = $baseSlug.'-'.Str::random(6);
} while (static::query()->where('slug', $slug)->exists());
$payload = $data;
$payload['user_id'] = $userId;
$payload['currency'] = ListingPanelHelper::normalizeCurrency($data['currency'] ?? null);
$payload['slug'] = $slug;
return static::query()->create($payload);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('listing-images');

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Modules\Listing\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use Modules\Listing\Models\Listing;
use Illuminate\Auth\Access\HandlesAuthorization;
class ListingPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:Listing');
}
public function view(AuthUser $authUser, Listing $listing): bool
{
return $authUser->can('View:Listing');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:Listing');
}
public function update(AuthUser $authUser, Listing $listing): bool
{
return $authUser->can('Update:Listing');
}
public function delete(AuthUser $authUser, Listing $listing): bool
{
return $authUser->can('Delete:Listing');
}
public function restore(AuthUser $authUser, Listing $listing): bool
{
return $authUser->can('Restore:Listing');
}
public function forceDelete(AuthUser $authUser, Listing $listing): bool
{
return $authUser->can('ForceDelete:Listing');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:Listing');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:Listing');
}
public function replicate(AuthUser $authUser, Listing $listing): bool
{
return $authUser->can('Replicate:Listing');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:Listing');
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Modules\Listing\Support;
use App\Settings\GeneralSettings;
use Throwable;
class ListingPanelHelper
{
public static function currencyCodes(): array
{
$codes = collect(config('app.currencies', ['USD']))
->filter(fn ($code) => is_string($code) && trim($code) !== '')
->map(fn (string $code) => strtoupper(substr(trim($code), 0, 3)))
->filter(fn (string $code) => strlen($code) === 3)
->unique()
->values()
->all();
return $codes !== [] ? $codes : ['USD'];
}
public static function currencyOptions(): array
{
return collect(self::currencyCodes())
->mapWithKeys(fn (string $code) => [$code => $code])
->all();
}
public static function defaultCurrency(): string
{
return self::currencyCodes()[0] ?? 'USD';
}
public static function normalizeCurrency(null | string $currency): string
{
$normalized = strtoupper(substr(trim((string) $currency), 0, 3));
$codes = self::currencyCodes();
if (in_array($normalized, $codes, true)) {
return $normalized;
}
return self::defaultCurrency();
}
public static function googleMapsEnabled(): bool
{
try {
return (bool) app(GeneralSettings::class)->enable_google_maps;
} catch (Throwable) {
return false;
}
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Modules\Location\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use Modules\Location\Models\Country;
use Illuminate\Auth\Access\HandlesAuthorization;
class CountryPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:Country');
}
public function view(AuthUser $authUser, Country $country): bool
{
return $authUser->can('View:Country');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:Country');
}
public function update(AuthUser $authUser, Country $country): bool
{
return $authUser->can('Update:Country');
}
public function delete(AuthUser $authUser, Country $country): bool
{
return $authUser->can('Delete:Country');
}
public function restore(AuthUser $authUser, Country $country): bool
{
return $authUser->can('Restore:Country');
}
public function forceDelete(AuthUser $authUser, Country $country): bool
{
return $authUser->can('ForceDelete:Country');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:Country');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:Country');
}
public function replicate(AuthUser $authUser, Country $country): bool
{
return $authUser->can('Replicate:Country');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:Country');
}
}

View File

@ -4,7 +4,7 @@ namespace Modules\Partner\Filament\Resources;
use A909M\FilamentStateFusion\Forms\Components\StateFusionSelect;
use A909M\FilamentStateFusion\Tables\Columns\StateFusionSelectColumn;
use A909M\FilamentStateFusion\Tables\Filters\StateFusionSelectFilter;
use App\Settings\GeneralSettings;
use App\Support\CountryCodeManager;
use BackedEnum;
use Cheesegrits\FilamentGoogleMaps\Fields\Map;
use Filament\Actions\Action;
@ -23,8 +23,9 @@ use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Modules\Category\Models\Category;
use Modules\Listing\Models\Listing;
use Modules\Listing\Support\ListingPanelHelper;
use Modules\Partner\Filament\Resources\ListingResource\Pages;
use Throwable;
use Tapp\FilamentCountryCodeField\Forms\Components\CountryCodeSelect;
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
class ListingResource extends Resource
@ -38,27 +39,32 @@ class ListingResource extends Resource
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()->prefix('$'),
TextInput::make('price')
->numeric()
->currencyMask(thousandSeparator: ',', decimalSeparator: '.', precision: 2),
Select::make('currency')
->options(fn () => self::currencyOptions())
->default(fn () => self::defaultCurrency())
->options(fn () => ListingPanelHelper::currencyOptions())
->default(fn () => ListingPanelHelper::defaultCurrency())
->required(),
Select::make('category_id')->label('Category')->options(fn () => Category::where('is_active', true)->pluck('name', 'id'))->searchable()->nullable(),
StateFusionSelect::make('status')->required(),
PhoneInput::make('contact_phone')->defaultCountry('TR')->nullable(),
PhoneInput::make('contact_phone')->defaultCountry(CountryCodeManager::defaultCountryIso2())->nullable(),
TextInput::make('contact_email')->email()->maxLength(255),
TextInput::make('city')->maxLength(100),
TextInput::make('country')->maxLength(100),
CountryCodeSelect::make('country')
->label('Country')
->default(fn () => CountryCodeManager::defaultCountryCode())
->formatStateUsing(fn ($state): ?string => CountryCodeManager::countryCodeFromLabelOrCode($state))
->dehydrateStateUsing(fn ($state, ?Listing $record): ?string => CountryCodeManager::normalizeStoredCountry($state ?? $record?->country)),
Map::make('location')
->label('Location')
->visible(fn (): bool => self::googleMapsEnabled())
->visible(fn (): bool => ListingPanelHelper::googleMapsEnabled())
->draggable()
->clickable()
->autocomplete('city')
->autocompleteReverse(true)
->reverseGeocode([
'city' => '%L',
'country' => '%C',
])
->defaultLocation([41.0082, 28.9784])
->defaultZoom(10)
@ -80,7 +86,9 @@ class ListingResource extends Resource
->circular(),
TextColumn::make('title')->searchable()->sortable()->limit(40),
TextColumn::make('category.name')->label('Category'),
TextColumn::make('price')->money('USD')->sortable(),
TextColumn::make('price')
->currency(fn (Listing $record): string => $record->currency ?: ListingPanelHelper::defaultCurrency())
->sortable(),
StateFusionSelectColumn::make('status'),
TextColumn::make('city'),
TextColumn::make('created_at')->dateTime()->sortable(),
@ -109,35 +117,4 @@ class ListingResource extends Resource
'edit' => Pages\EditListing::route('/{record}/edit'),
];
}
private static function currencyOptions(): array
{
$codes = collect(config('app.currencies', ['USD']))
->filter(fn ($code) => is_string($code) && trim($code) !== '')
->map(fn (string $code) => strtoupper(substr(trim($code), 0, 3)))
->filter(fn (string $code) => strlen($code) === 3)
->unique()
->values()
->all();
if ($codes === []) {
$codes = ['USD'];
}
return collect($codes)->mapWithKeys(fn (string $code) => [$code => $code])->all();
}
private static function defaultCurrency(): string
{
return array_key_first(self::currencyOptions()) ?? 'USD';
}
private static function googleMapsEnabled(): bool
{
try {
return (bool) app(GeneralSettings::class)->enable_google_maps;
} catch (Throwable) {
return false;
}
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use TallCms\Cms\Models\CmsCategory;
use Illuminate\Auth\Access\HandlesAuthorization;
class CmsCategoryPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:CmsCategory');
}
public function view(AuthUser $authUser, CmsCategory $cmsCategory): bool
{
return $authUser->can('View:CmsCategory');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:CmsCategory');
}
public function update(AuthUser $authUser, CmsCategory $cmsCategory): bool
{
return $authUser->can('Update:CmsCategory');
}
public function delete(AuthUser $authUser, CmsCategory $cmsCategory): bool
{
return $authUser->can('Delete:CmsCategory');
}
public function restore(AuthUser $authUser, CmsCategory $cmsCategory): bool
{
return $authUser->can('Restore:CmsCategory');
}
public function forceDelete(AuthUser $authUser, CmsCategory $cmsCategory): bool
{
return $authUser->can('ForceDelete:CmsCategory');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:CmsCategory');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:CmsCategory');
}
public function replicate(AuthUser $authUser, CmsCategory $cmsCategory): bool
{
return $authUser->can('Replicate:CmsCategory');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:CmsCategory');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use TallCms\Cms\Models\CmsComment;
use Illuminate\Auth\Access\HandlesAuthorization;
class CmsCommentPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:CmsComment');
}
public function view(AuthUser $authUser, CmsComment $cmsComment): bool
{
return $authUser->can('View:CmsComment');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:CmsComment');
}
public function update(AuthUser $authUser, CmsComment $cmsComment): bool
{
return $authUser->can('Update:CmsComment');
}
public function delete(AuthUser $authUser, CmsComment $cmsComment): bool
{
return $authUser->can('Delete:CmsComment');
}
public function restore(AuthUser $authUser, CmsComment $cmsComment): bool
{
return $authUser->can('Restore:CmsComment');
}
public function forceDelete(AuthUser $authUser, CmsComment $cmsComment): bool
{
return $authUser->can('ForceDelete:CmsComment');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:CmsComment');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:CmsComment');
}
public function replicate(AuthUser $authUser, CmsComment $cmsComment): bool
{
return $authUser->can('Replicate:CmsComment');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:CmsComment');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use TallCms\Cms\Models\CmsPage;
use Illuminate\Auth\Access\HandlesAuthorization;
class CmsPagePolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:CmsPage');
}
public function view(AuthUser $authUser, CmsPage $cmsPage): bool
{
return $authUser->can('View:CmsPage');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:CmsPage');
}
public function update(AuthUser $authUser, CmsPage $cmsPage): bool
{
return $authUser->can('Update:CmsPage');
}
public function delete(AuthUser $authUser, CmsPage $cmsPage): bool
{
return $authUser->can('Delete:CmsPage');
}
public function restore(AuthUser $authUser, CmsPage $cmsPage): bool
{
return $authUser->can('Restore:CmsPage');
}
public function forceDelete(AuthUser $authUser, CmsPage $cmsPage): bool
{
return $authUser->can('ForceDelete:CmsPage');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:CmsPage');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:CmsPage');
}
public function replicate(AuthUser $authUser, CmsPage $cmsPage): bool
{
return $authUser->can('Replicate:CmsPage');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:CmsPage');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use TallCms\Cms\Models\CmsPost;
use Illuminate\Auth\Access\HandlesAuthorization;
class CmsPostPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:CmsPost');
}
public function view(AuthUser $authUser, CmsPost $cmsPost): bool
{
return $authUser->can('View:CmsPost');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:CmsPost');
}
public function update(AuthUser $authUser, CmsPost $cmsPost): bool
{
return $authUser->can('Update:CmsPost');
}
public function delete(AuthUser $authUser, CmsPost $cmsPost): bool
{
return $authUser->can('Delete:CmsPost');
}
public function restore(AuthUser $authUser, CmsPost $cmsPost): bool
{
return $authUser->can('Restore:CmsPost');
}
public function forceDelete(AuthUser $authUser, CmsPost $cmsPost): bool
{
return $authUser->can('ForceDelete:CmsPost');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:CmsPost');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:CmsPost');
}
public function replicate(AuthUser $authUser, CmsPost $cmsPost): bool
{
return $authUser->can('Replicate:CmsPost');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:CmsPost');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use TallCms\Cms\Models\MediaCollection;
use Illuminate\Auth\Access\HandlesAuthorization;
class MediaCollectionPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:MediaCollection');
}
public function view(AuthUser $authUser, MediaCollection $mediaCollection): bool
{
return $authUser->can('View:MediaCollection');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:MediaCollection');
}
public function update(AuthUser $authUser, MediaCollection $mediaCollection): bool
{
return $authUser->can('Update:MediaCollection');
}
public function delete(AuthUser $authUser, MediaCollection $mediaCollection): bool
{
return $authUser->can('Delete:MediaCollection');
}
public function restore(AuthUser $authUser, MediaCollection $mediaCollection): bool
{
return $authUser->can('Restore:MediaCollection');
}
public function forceDelete(AuthUser $authUser, MediaCollection $mediaCollection): bool
{
return $authUser->can('ForceDelete:MediaCollection');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:MediaCollection');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:MediaCollection');
}
public function replicate(AuthUser $authUser, MediaCollection $mediaCollection): bool
{
return $authUser->can('Replicate:MediaCollection');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:MediaCollection');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use Spatie\Permission\Models\Role;
use Illuminate\Auth\Access\HandlesAuthorization;
class RolePolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:Role');
}
public function view(AuthUser $authUser, Role $role): bool
{
return $authUser->can('View:Role');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:Role');
}
public function update(AuthUser $authUser, Role $role): bool
{
return $authUser->can('Update:Role');
}
public function delete(AuthUser $authUser, Role $role): bool
{
return $authUser->can('Delete:Role');
}
public function restore(AuthUser $authUser, Role $role): bool
{
return $authUser->can('Restore:Role');
}
public function forceDelete(AuthUser $authUser, Role $role): bool
{
return $authUser->can('ForceDelete:Role');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:Role');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:Role');
}
public function replicate(AuthUser $authUser, Role $role): bool
{
return $authUser->can('Replicate:Role');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:Role');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use TallCms\Cms\Models\TallcmsContactSubmission;
use Illuminate\Auth\Access\HandlesAuthorization;
class TallcmsContactSubmissionPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:TallcmsContactSubmission');
}
public function view(AuthUser $authUser, TallcmsContactSubmission $tallcmsContactSubmission): bool
{
return $authUser->can('View:TallcmsContactSubmission');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:TallcmsContactSubmission');
}
public function update(AuthUser $authUser, TallcmsContactSubmission $tallcmsContactSubmission): bool
{
return $authUser->can('Update:TallcmsContactSubmission');
}
public function delete(AuthUser $authUser, TallcmsContactSubmission $tallcmsContactSubmission): bool
{
return $authUser->can('Delete:TallcmsContactSubmission');
}
public function restore(AuthUser $authUser, TallcmsContactSubmission $tallcmsContactSubmission): bool
{
return $authUser->can('Restore:TallcmsContactSubmission');
}
public function forceDelete(AuthUser $authUser, TallcmsContactSubmission $tallcmsContactSubmission): bool
{
return $authUser->can('ForceDelete:TallcmsContactSubmission');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:TallcmsContactSubmission');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:TallcmsContactSubmission');
}
public function replicate(AuthUser $authUser, TallcmsContactSubmission $tallcmsContactSubmission): bool
{
return $authUser->can('Replicate:TallcmsContactSubmission');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:TallcmsContactSubmission');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use TallCms\Cms\Models\TallcmsMedia;
use Illuminate\Auth\Access\HandlesAuthorization;
class TallcmsMediaPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:TallcmsMedia');
}
public function view(AuthUser $authUser, TallcmsMedia $tallcmsMedia): bool
{
return $authUser->can('View:TallcmsMedia');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:TallcmsMedia');
}
public function update(AuthUser $authUser, TallcmsMedia $tallcmsMedia): bool
{
return $authUser->can('Update:TallcmsMedia');
}
public function delete(AuthUser $authUser, TallcmsMedia $tallcmsMedia): bool
{
return $authUser->can('Delete:TallcmsMedia');
}
public function restore(AuthUser $authUser, TallcmsMedia $tallcmsMedia): bool
{
return $authUser->can('Restore:TallcmsMedia');
}
public function forceDelete(AuthUser $authUser, TallcmsMedia $tallcmsMedia): bool
{
return $authUser->can('ForceDelete:TallcmsMedia');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:TallcmsMedia');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:TallcmsMedia');
}
public function replicate(AuthUser $authUser, TallcmsMedia $tallcmsMedia): bool
{
return $authUser->can('Replicate:TallcmsMedia');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:TallcmsMedia');
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use TallCms\Cms\Models\TallcmsMenu;
use Illuminate\Auth\Access\HandlesAuthorization;
class TallcmsMenuPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:TallcmsMenu');
}
public function view(AuthUser $authUser, TallcmsMenu $tallcmsMenu): bool
{
return $authUser->can('View:TallcmsMenu');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:TallcmsMenu');
}
public function update(AuthUser $authUser, TallcmsMenu $tallcmsMenu): bool
{
return $authUser->can('Update:TallcmsMenu');
}
public function delete(AuthUser $authUser, TallcmsMenu $tallcmsMenu): bool
{
return $authUser->can('Delete:TallcmsMenu');
}
public function restore(AuthUser $authUser, TallcmsMenu $tallcmsMenu): bool
{
return $authUser->can('Restore:TallcmsMenu');
}
public function forceDelete(AuthUser $authUser, TallcmsMenu $tallcmsMenu): bool
{
return $authUser->can('ForceDelete:TallcmsMenu');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:TallcmsMenu');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:TallcmsMenu');
}
public function replicate(AuthUser $authUser, TallcmsMenu $tallcmsMenu): bool
{
return $authUser->can('Replicate:TallcmsMenu');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:TallcmsMenu');
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:User');
}
public function view(AuthUser $authUser): bool
{
return $authUser->can('View:User');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:User');
}
public function update(AuthUser $authUser): bool
{
return $authUser->can('Update:User');
}
public function delete(AuthUser $authUser): bool
{
return $authUser->can('Delete:User');
}
public function restore(AuthUser $authUser): bool
{
return $authUser->can('Restore:User');
}
public function forceDelete(AuthUser $authUser): bool
{
return $authUser->can('ForceDelete:User');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:User');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:User');
}
public function replicate(AuthUser $authUser): bool
{
return $authUser->can('Replicate:User');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:User');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Support\CountryCodeManager;
use App\Settings\GeneralSettings;
use BezhanSalleh\LanguageSwitch\LanguageSwitch;
use Illuminate\Support\ServiceProvider;
@ -32,12 +33,14 @@ class AppServiceProvider extends ServiceProvider
$fallbackFacebookClientSecret = env('FACEBOOK_CLIENT_SECRET');
$fallbackAppleClientId = env('APPLE_CLIENT_ID');
$fallbackAppleClientSecret = env('APPLE_CLIENT_SECRET');
$fallbackDefaultCountryCode = '+90';
$generalSettings = [
'site_name' => $fallbackName,
'site_description' => $fallbackDescription,
'site_logo_url' => null,
'default_language' => $fallbackLocale,
'default_country_code' => $fallbackDefaultCountryCode,
'currencies' => $fallbackCurrencies,
'sender_email' => config('mail.from.address', 'hello@example.com'),
'sender_name' => config('mail.from.name', $fallbackName),
@ -81,6 +84,7 @@ class AppServiceProvider extends ServiceProvider
$facebookClientSecret = trim((string) ($settings->facebook_client_secret ?: $fallbackFacebookClientSecret));
$appleClientId = trim((string) ($settings->apple_client_id ?: $fallbackAppleClientId));
$appleClientSecret = trim((string) ($settings->apple_client_secret ?: $fallbackAppleClientSecret));
$defaultCountryCode = CountryCodeManager::normalizeCountryCode($settings->default_country_code ?? $fallbackDefaultCountryCode);
$generalSettings = [
'site_name' => trim((string) ($settings->site_name ?: $fallbackName)),
@ -89,6 +93,7 @@ class AppServiceProvider extends ServiceProvider
? Storage::disk('public')->url($settings->site_logo)
: null,
'default_language' => $defaultLanguage,
'default_country_code' => $defaultCountryCode,
'currencies' => $currencies,
'sender_email' => trim((string) ($settings->sender_email ?: config('mail.from.address'))),
'sender_name' => trim((string) ($settings->sender_name ?: $fallbackName)),
@ -143,6 +148,9 @@ class AppServiceProvider extends ServiceProvider
'services.apple.redirect' => url('/oauth/callback/apple'),
'services.apple.stateless' => true,
'services.apple.enabled' => (bool) $generalSettings['apple_login_enabled'],
'money.defaults.currency' => $generalSettings['currencies'][0] ?? 'USD',
'app.default_country_code' => $generalSettings['default_country_code'] ?? $fallbackDefaultCountryCode,
'app.default_country_iso2' => CountryCodeManager::iso2FromCountryCode($generalSettings['default_country_code'] ?? $fallbackDefaultCountryCode) ?? 'TR',
]);
Event::listen(function (SocialiteWasCalled $event): void {

View File

@ -14,6 +14,8 @@ class GeneralSettings extends Settings
public string $default_language;
public string $default_country_code;
public array $currencies;
public string $sender_email;

View File

@ -0,0 +1,168 @@
<?php
namespace App\Support;
use Illuminate\Support\Collection;
use Tapp\FilamentCountryCodeField\Enums\CountriesEnum;
class CountryCodeManager
{
public static function defaultCountryCode(): string
{
return self::normalizeCountryCode(config('app.default_country_code', '+90'));
}
public static function defaultCountryIso2(): string
{
return self::iso2FromCountryCode(self::defaultCountryCode()) ?? 'TR';
}
public static function normalizeCountryCode(null | string $value): string
{
$value = trim((string) $value);
if ($value === '') {
return '+90';
}
if (self::isValidCountryCode($value)) {
return $value;
}
return self::countryCodeFromIso2($value) ?? '+90';
}
public static function isValidCountryCode(null | string $value): bool
{
if (! filled($value)) {
return false;
}
return self::countries()->contains(fn (array $country): bool => $country['country_code'] === trim((string) $value));
}
public static function countryCodeFromIso2(null | string $iso2): ?string
{
$iso2 = strtoupper(trim((string) $iso2));
if ($iso2 === '') {
return null;
}
return self::countries()
->first(fn (array $country): bool => $country['iso2'] === $iso2)['country_code'] ?? null;
}
public static function iso2FromCountryCode(null | string $countryCode): ?string
{
$countryCode = trim((string) $countryCode);
if ($countryCode === '') {
return null;
}
return self::countries()
->first(fn (array $country): bool => $country['country_code'] === $countryCode)['iso2'] ?? null;
}
public static function labelFromCountryCode(null | string $countryCode): ?string
{
$countryCode = trim((string) $countryCode);
if ($countryCode === '') {
return null;
}
return self::countries()
->first(fn (array $country): bool => $country['country_code'] === $countryCode)['english_label'] ?? null;
}
public static function countryCodeFromLabelOrCode(null | string $value): ?string
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
if (self::isValidCountryCode($value)) {
return $value;
}
$fromIso = self::countryCodeFromIso2($value);
if ($fromIso) {
return $fromIso;
}
$normalizedInput = self::normalizeLabel($value);
return self::countries()
->first(function (array $country) use ($normalizedInput): bool {
$normalizedLabel = self::normalizeLabel($country['label']);
$normalizedEnglish = self::normalizeLabel($country['english_label']);
return $normalizedInput === $normalizedLabel
|| $normalizedInput === $normalizedEnglish
|| str_contains($normalizedLabel, $normalizedInput)
|| str_contains($normalizedEnglish, $normalizedInput)
|| str_contains($normalizedInput, $normalizedLabel)
|| str_contains($normalizedInput, $normalizedEnglish);
})['country_code'] ?? null;
}
public static function normalizeStoredCountry(null | string $value): ?string
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
$countryCode = self::countryCodeFromLabelOrCode($value);
if (! $countryCode) {
return $value;
}
return self::labelFromCountryCode($countryCode) ?? $value;
}
/**
* @return Collection<int, array{country_code: string, iso2: string, label: string, english_label: string}>
*/
private static function countries(): Collection
{
static $countries;
if ($countries instanceof Collection) {
return $countries;
}
$countries = collect(CountriesEnum::cases())
->map(function (CountriesEnum $country): array {
$countryKey = $country->value;
$iso2 = strtoupper(explode('_', $countryKey)[0] ?? $countryKey);
$label = (string) trans("filament-country-code-field::countries.{$countryKey}");
$englishLabel = (string) trans("filament-country-code-field::countries.{$countryKey}", [], 'en');
return [
'country_code' => $country->getCountryCode(),
'iso2' => $iso2,
'label' => $label,
'english_label' => $englishLabel,
];
})
->values();
return $countries;
}
private static function normalizeLabel(string $value): string
{
$value = mb_strtolower(trim($value));
$value = preg_replace('/[^a-z0-9]+/u', ' ', $value) ?? $value;
return trim(preg_replace('/\s+/', ' ', $value) ?? $value);
}
}

View File

@ -8,6 +8,7 @@
"require": {
"php": "^8.2",
"a909m/filament-statefusion": "^2.3",
"ariaieboy/filament-currency": "^3.0",
"bezhansalleh/filament-language-switch": "^4.1",
"cheesegrits/filament-google-maps": "^5.0",
"dutchcodingcompany/filament-developer-logins": "^2.1",
@ -23,9 +24,11 @@
"nwidart/laravel-modules": "^11.0",
"pxlrbt/filament-activity-log": "^2.1",
"socialiteproviders/apple": "^5.9",
"spatie/laravel-permission": "^7.2",
"spatie/laravel-permission": "^6.24",
"spatie/laravel-settings": "^3.7",
"stechstudio/filament-impersonate": "^5.1",
"tallcms/cms": "^3.2",
"tapp/filament-country-code-field": "^2.0",
"ysfkaya/filament-phone-input": "^4.1"
},
"require-dev": {

1843
config/money.php Normal file

File diff suppressed because it is too large Load Diff

569
config/tallcms.php Normal file
View File

@ -0,0 +1,569 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| TallCMS Version
|--------------------------------------------------------------------------
|
| The current version of TallCMS. Read dynamically from composer.json
| to ensure it's always in sync with the installed package version.
|
*/
'version' => (function () {
$composerJson = dirname(__DIR__).'/composer.json';
if (file_exists($composerJson)) {
$data = json_decode(file_get_contents($composerJson), true);
return $data['version'] ?? 'unknown';
}
return 'unknown';
})(),
/*
|--------------------------------------------------------------------------
| Operation Mode
|--------------------------------------------------------------------------
|
| Determines how TallCMS operates. Auto-detection works in most cases:
| - 'standalone': Full TallCMS installation (tallcms/tallcms skeleton)
| - 'plugin': Installed as a plugin in existing Filament app
| - null: Auto-detect based on .tallcms-standalone marker file
|
*/
'mode' => env('TALLCMS_MODE'),
/*
|--------------------------------------------------------------------------
| Database Configuration
|--------------------------------------------------------------------------
|
| Table prefix for all TallCMS tables. Default 'tallcms_' maintains
| compatibility with v1.x installations. Can be customized in plugin
| mode to avoid conflicts with existing tables.
|
*/
'database' => [
'prefix' => env('TALLCMS_TABLE_PREFIX', 'tallcms_'),
],
/*
|--------------------------------------------------------------------------
| Plugin Mode Settings
|--------------------------------------------------------------------------
|
| Configuration specific to plugin mode operation. These settings are
| ignored in standalone mode.
|
*/
'plugin_mode' => [
// Enable frontend CMS page routes.
// When enabled, TallCMS registers both / (homepage) and /{slug} routes.
// WARNING: Without a prefix, this will override your app's homepage route!
'routes_enabled' => env('TALLCMS_ROUTES_ENABLED', false),
// Optional URL prefix for CMS routes (e.g., 'cms' results in /cms and /cms/{slug})
// Leave empty for root-level routes (/, /about, /contact)
// When empty, smart exclusions prevent conflicts with your app routes.
'routes_prefix' => env('TALLCMS_ROUTES_PREFIX', ''),
// Route name prefix for plugin mode (e.g., 'tallcms.' results in tallcms.cms.page)
'route_name_prefix' => env('TALLCMS_PLUGIN_ROUTE_NAME_PREFIX', 'tallcms.'),
// Route exclusion pattern - paths matching this regex are excluded from CMS routing.
// Default excludes common Laravel/Filament paths. Panel path is auto-excluded.
//
// In NON-i18n mode with standard format (^(?!foo|bar).*$): Merged with base exclusions.
// In NON-i18n mode with custom regex: Used as-is, replaces default pattern entirely.
// NOTE: When using custom regex, 'additional_exclusions' is ignored.
// In i18n mode: Only standard negative lookahead format is merged; other formats ignored.
'route_exclusions' => env('TALLCMS_PLUGIN_ROUTE_EXCLUSIONS',
env('TALLCMS_ROUTE_EXCLUSIONS', // backward compat
'^(?!admin|app|api|livewire|sanctum|storage|build|vendor|health|_).*$'
)
),
// Additional route exclusions as pipe-separated list (e.g., 'dashboard|settings|profile').
// Merged with base exclusions when using standard route_exclusions format.
// NOTE: Ignored when route_exclusions is set to a non-standard custom regex.
// Recommended for i18n mode where custom regex is not supported.
'additional_exclusions' => env('TALLCMS_ADDITIONAL_EXCLUSIONS', ''),
// Enable preview routes (/preview/page/{id}, /preview/post/{id})
'preview_routes_enabled' => env('TALLCMS_PREVIEW_ROUTES_ENABLED', true),
// Enable API routes (/api/contact)
'api_routes_enabled' => env('TALLCMS_API_ROUTES_ENABLED', true),
// Optional prefix for essential routes (preview, contact API) to avoid conflicts
// e.g., 'tallcms' results in /tallcms/preview/page/{id}
'essential_routes_prefix' => env('TALLCMS_ESSENTIAL_ROUTES_PREFIX', ''),
// Enable core SEO routes (sitemap.xml, robots.txt).
// These are always registered at root level (no prefix) since search
// engines expect them at standard locations. Safe to enable.
'seo_routes_enabled' => env('TALLCMS_SEO_ROUTES_ENABLED', true),
// Enable archive routes (RSS feed, category archives, author archives).
// These routes (/feed, /category/{slug}, /author/{slug}) may conflict
// with your app's routes. Disabled by default in plugin mode.
'archive_routes_enabled' => env('TALLCMS_ARCHIVE_ROUTES_ENABLED', false),
// Optional prefix for archive routes to avoid conflicts.
// e.g., 'blog' results in /blog/feed, /blog/category/{slug}, /blog/author/{slug}
'archive_routes_prefix' => env('TALLCMS_ARCHIVE_ROUTES_PREFIX', ''),
// Enable the TallCMS plugin system.
// When enabled, the Plugin Manager page is visible and third-party plugins can be loaded.
'plugins_enabled' => env('TALLCMS_PLUGINS_ENABLED', true),
// Enable the TallCMS theme system.
// When enabled, the Theme Manager page is visible and themes can be loaded.
'themes_enabled' => env('TALLCMS_THEMES_ENABLED', true),
// User model class. Must implement TallCmsUserContract.
// Default works with standard Laravel User model with HasRoles trait.
'user_model' => env('TALLCMS_USER_MODEL', 'App\\Models\\User'),
// Skip installer.lock check for maintenance mode in plugin mode.
// In plugin mode, the host app doesn't use TallCMS's installer,
// so we assume the app is properly installed. Default: true
'skip_installer_check' => env('TALLCMS_SKIP_INSTALLER_CHECK', true),
],
/*
|--------------------------------------------------------------------------
| Authentication Configuration
|--------------------------------------------------------------------------
|
| Configuration for authentication guards used by TallCMS roles and
| permissions. This should match your Filament panel's guard.
|
*/
'auth' => [
// Guard name for roles and permissions (should match Filament panel guard)
'guard' => env('TALLCMS_AUTH_GUARD', 'web'),
// Login route for preview authentication redirect
// Can be a route name (e.g., 'filament.admin.auth.login') or URL
// Leave null to auto-detect Filament's login route
'login_route' => env('TALLCMS_LOGIN_ROUTE'),
],
/*
|--------------------------------------------------------------------------
| Filament Panel Configuration
|--------------------------------------------------------------------------
|
| These settings are dynamically set by TallCmsPlugin when registered.
| They allow customization of navigation group and sort order.
|
*/
'filament' => [
// Panel ID for route generation in notifications
// Used for constructing admin panel URLs like filament.{panel_id}.resources.*
'panel_id' => env('TALLCMS_PANEL_ID', 'admin'),
// Panel path for URL construction and middleware exclusions
'panel_path' => env('TALLCMS_PANEL_PATH', 'admin'),
// Navigation group override - when set, CMS resources/pages use this group.
// Note: UserResource stays in 'User Management' regardless of this setting.
// Leave unset (null) to use per-resource defaults (Content Management, Settings, etc.)
'navigation_group' => env('TALLCMS_NAVIGATION_GROUP'),
// Navigation sort override - when set, CMS resources/pages use this sort.
// Leave unset (null) to use per-resource defaults.
'navigation_sort' => env('TALLCMS_NAVIGATION_SORT') !== null
? (int) env('TALLCMS_NAVIGATION_SORT')
: null,
],
/*
|--------------------------------------------------------------------------
| Contact Information
|--------------------------------------------------------------------------
|
| Default contact information used in templates and merge tags.
|
*/
'contact_email' => env('TALLCMS_CONTACT_EMAIL'),
'company_name' => env('TALLCMS_COMPANY_NAME'),
'company_address' => env('TALLCMS_COMPANY_ADDRESS'),
/*
|--------------------------------------------------------------------------
| Publishing Workflow
|--------------------------------------------------------------------------
|
| Configuration for the content publishing workflow including
| revision history and preview tokens.
|
*/
'publishing' => [
// Maximum number of automatic revisions to keep per content item.
// Set to null for unlimited. Default: 100
'revision_limit' => env('CMS_REVISION_LIMIT', 100),
// Maximum number of manual (pinned) snapshots to keep per content item.
// Set to null for unlimited. Default: 50
'revision_manual_limit' => env('CMS_REVISION_MANUAL_LIMIT', 50),
// Notification channels for workflow events
// Available: 'mail', 'database'
'notification_channels' => explode(',', env('CMS_NOTIFICATION_CHANNELS', 'mail,database')),
// Default preview token expiry in hours
'default_preview_expiry_hours' => 24,
],
/*
|--------------------------------------------------------------------------
| Plugin System
|--------------------------------------------------------------------------
|
| Configuration for the TallCMS plugin system including license management.
| The Plugin Manager UI is always available, but local plugin loading
| requires explicit opt-in via plugin_mode.plugins_enabled.
|
*/
'plugins' => [
// Path where plugins are stored
'path' => env('TALLCMS_PLUGINS_PATH', base_path('plugins')),
// Allow ZIP-based plugin uploads through admin UI
'allow_uploads' => env('TALLCMS_PLUGIN_ALLOW_UPLOADS', env('PLUGIN_ALLOW_UPLOADS', true)),
// Maximum upload size for plugin ZIP files (bytes). Default: 50MB
'max_upload_size' => env('TALLCMS_PLUGIN_MAX_UPLOAD_SIZE', env('PLUGIN_MAX_UPLOAD_SIZE', 50 * 1024 * 1024)),
// Plugin discovery caching
'cache_enabled' => env('TALLCMS_PLUGIN_CACHE_ENABLED', env('PLUGIN_CACHE_ENABLED', true)),
'cache_ttl' => 3600, // 1 hour
// Automatically run plugin migrations on install
'auto_migrate' => env('TALLCMS_PLUGIN_AUTO_MIGRATE', env('PLUGIN_AUTO_MIGRATE', true)),
// License management settings
'license' => [
// License proxy URL for official TallCMS plugins
'proxy_url' => env('TALLCMS_LICENSE_PROXY_URL', 'https://tallcms.com'),
// Cache TTL for license validation results (seconds). Default: 6 hours
'cache_ttl' => 21600,
// Grace period when license server unreachable (days). Default: 7
'offline_grace_days' => 7,
// Grace period after license expiration (days). Default: 14
'renewal_grace_days' => 14,
// How often to check for updates (seconds). Default: 24 hours
'update_check_interval' => 86400,
// Purchase URLs for plugins (shown when no license is active)
'purchase_urls' => [
'tallcms/pro' => 'https://checkout.anystack.sh/tallcms-pro-plugin',
'tallcms/mega-menu' => 'https://checkout.anystack.sh/tallcms-mega-menu-plugin',
],
// Download URLs for plugins (shown when license is valid)
'download_urls' => [
'tallcms/pro' => 'https://anystack.sh/download/tallcms-pro-plugin',
'tallcms/mega-menu' => 'https://anystack.sh/download/tallcms-mega-menu-plugin',
],
],
// Official plugin catalog (shown in Plugin Manager)
'catalog' => [
'tallcms/pro' => [
'name' => 'TallCMS Pro',
'slug' => 'pro',
'vendor' => 'tallcms',
'description' => 'Advanced blocks, analytics, and integrations for TallCMS.',
'author' => 'TallCMS',
'homepage' => 'https://tallcms.com/pro',
'icon' => 'heroicon-o-sparkles',
'category' => 'official',
'featured' => true,
'download_url' => 'https://anystack.sh/download/tallcms-pro-plugin',
'purchase_url' => 'https://checkout.anystack.sh/tallcms-pro-plugin',
],
'tallcms/mega-menu' => [
'name' => 'TallCMS Mega Menu',
'slug' => 'mega-menu',
'vendor' => 'tallcms',
'description' => 'Create stunning mega menus for your website with ease. Build rich, multi-column dropdown menus with images, icons, and custom layouts.',
'author' => 'TallCMS',
'homepage' => 'https://tallcms.com/mega-menu',
'icon' => 'heroicon-o-bars-3-bottom-left',
'category' => 'official',
'featured' => true,
'download_url' => 'https://anystack.sh/download/tallcms-mega-menu-plugin',
'purchase_url' => 'https://checkout.anystack.sh/tallcms-mega-menu-plugin',
],
],
],
/*
|--------------------------------------------------------------------------
| Theme System
|--------------------------------------------------------------------------
|
| Configuration for the TallCMS theme system. The Theme Manager UI is
| always available, but theme loading requires explicit opt-in via
| plugin_mode.themes_enabled in plugin mode.
|
*/
'themes' => [
// Path where themes are stored
'path' => env('TALLCMS_THEMES_PATH', base_path('themes')),
// Allow ZIP-based theme uploads through admin UI
'allow_uploads' => env('TALLCMS_THEME_ALLOW_UPLOADS', true),
// Maximum upload size for theme ZIP files (bytes). Default: 100MB
'max_upload_size' => env('TALLCMS_THEME_MAX_UPLOAD_SIZE', 100 * 1024 * 1024),
// Theme discovery caching
'cache_enabled' => env('TALLCMS_THEME_CACHE_ENABLED', false),
'cache_ttl' => 3600, // 1 hour
// Preview session duration (minutes)
'preview_duration' => 30,
// Rollback availability window (hours)
'rollback_duration' => 24,
],
/*
|--------------------------------------------------------------------------
| REST API
|--------------------------------------------------------------------------
|
| Configuration for the TallCMS REST API. The API provides full CRUD
| operations for Pages, Posts, Categories, and Media with authentication
| via Laravel Sanctum tokens.
|
*/
'api' => [
// Enable or disable the REST API
'enabled' => env('TALLCMS_API_ENABLED', false),
// API route prefix (e.g., 'api/v1/tallcms' results in /api/v1/tallcms/pages)
'prefix' => env('TALLCMS_API_PREFIX', 'api/v1/tallcms'),
// Standard rate limit (requests per minute)
'rate_limit' => env('TALLCMS_API_RATE_LIMIT', 60),
// Authentication rate limit (failed attempts before lockout)
'auth_rate_limit' => env('TALLCMS_API_AUTH_RATE_LIMIT', 5),
// Authentication lockout duration (minutes)
'auth_lockout_minutes' => env('TALLCMS_API_AUTH_LOCKOUT', 15),
// Default token expiry (days)
'token_expiry_days' => env('TALLCMS_API_TOKEN_EXPIRY', 365),
// Maximum items per page for pagination
'max_per_page' => 100,
],
/*
|--------------------------------------------------------------------------
| Webhooks
|--------------------------------------------------------------------------
|
| Configuration for webhook delivery to external services. Webhooks notify
| external systems when content is created, updated, published, or deleted.
|
*/
'webhooks' => [
// Enable or disable webhooks
'enabled' => env('TALLCMS_WEBHOOKS_ENABLED', false),
// Request timeout (seconds)
'timeout' => env('TALLCMS_WEBHOOK_TIMEOUT', 30),
// Maximum retry attempts
'max_retries' => env('TALLCMS_WEBHOOK_MAX_RETRIES', 3),
// Delay before retry attempts (seconds) - retry 1, 2, 3
'retry_backoff' => [60, 300, 900],
// Maximum response body size to store (bytes)
'response_max_size' => 10000,
// Allowed hosts (empty = allow all public IPs)
'allowed_hosts' => [],
// Explicitly blocked hosts
'blocked_hosts' => [],
// Queue name for webhook jobs
'queue' => env('TALLCMS_WEBHOOK_QUEUE', 'default'),
],
/*
|--------------------------------------------------------------------------
| Internationalization (i18n)
|--------------------------------------------------------------------------
|
| Core i18n configuration. Locales are merged from multiple sources:
| - Config: Base locales (always available)
| - Plugins: Can ADD new locale codes (cannot override config)
| - DB: Can MODIFY existing locales (enable/disable/rename, cannot add)
|
*/
'i18n' => [
// Master switch for multilingual features
'enabled' => env('TALLCMS_I18N_ENABLED', false),
// Base locales (always available, plugins can add new ones, DB can modify existing)
'locales' => [
'en' => [
'label' => 'English',
'native' => 'English',
'rtl' => false,
],
'zh_CN' => [
'label' => 'Chinese (Simplified)',
'native' => '简体中文',
'rtl' => false,
],
],
// Default/fallback locale (must exist in registry)
'default_locale' => env('TALLCMS_DEFAULT_LOCALE', 'en'),
// URL strategy: 'prefix' (/en/about) or 'none' (query param fallback)
'url_strategy' => 'prefix',
// Hide default locale from URL (/ instead of /en/)
'hide_default_locale' => env('TALLCMS_HIDE_DEFAULT_LOCALE', true),
// Fallback when translation missing: 'default', 'empty', 'key'
'fallback_behavior' => 'default',
// Remember locale preference in session
'remember_locale' => true,
],
/*
|--------------------------------------------------------------------------
| Comments
|--------------------------------------------------------------------------
|
| Configuration for the blog post commenting system. Comments require
| admin approval before appearing publicly.
|
*/
'comments' => [
'enabled' => env('TALLCMS_COMMENTS_ENABLED', true),
'moderation' => env('TALLCMS_COMMENTS_MODERATION', 'manual'), // 'manual' = require approval, 'auto' = publish immediately
'max_depth' => 2, // top-level + 1 reply level (min 1)
'max_length' => 5000, // max comment content length
'rate_limit' => 5, // max comments per IP per window
'rate_limit_decay' => 600, // rate limit window in seconds
'notification_channels' => ['mail', 'database'],
'notify_on_approval' => true, // email commenter when approved
'guest_comments' => true, // allow non-authenticated comments
],
/*
|--------------------------------------------------------------------------
| Media Library
|--------------------------------------------------------------------------
|
| Configuration for media library features including image optimization,
| variant generation, and responsive image handling.
|
*/
'media' => [
'optimization' => [
// Enable or disable automatic image optimization
'enabled' => env('TALLCMS_MEDIA_OPTIMIZATION', true),
// Queue name for optimization jobs
'queue' => env('TALLCMS_MEDIA_QUEUE', 'default'),
// WebP quality (0-100)
'quality' => env('TALLCMS_MEDIA_QUALITY', 80),
// Variant presets - customize sizes as needed
'variants' => [
'thumbnail' => ['width' => 300, 'height' => 300, 'fit' => 'crop'],
'medium' => ['width' => 800, 'height' => 600, 'fit' => 'contain'],
'large' => ['width' => 1200, 'height' => 800, 'fit' => 'contain'],
],
],
],
/*
|--------------------------------------------------------------------------
| Full-Text Search
|--------------------------------------------------------------------------
|
| Configuration for the full-text search functionality using Laravel Scout.
| Requires SCOUT_DRIVER=database in your .env file.
|
*/
'search' => [
// Enable or disable search functionality
'enabled' => env('TALLCMS_SEARCH_ENABLED', true),
// Minimum query length required before searching
'min_query_length' => 2,
// Number of results per page on the search results page
'results_per_page' => 10,
// Maximum results per model type to avoid memory issues
'max_results_per_type' => 50,
// Which content types to include in search
'searchable_types' => ['pages', 'posts'],
],
/*
|--------------------------------------------------------------------------
| System Updates (Standalone Mode Only)
|--------------------------------------------------------------------------
|
| Configuration for the one-click update system. These settings are
| IGNORED in plugin mode - use Composer for updates instead.
|
*/
'updates' => [
// Enable or disable the update system (standalone mode only)
'enabled' => env('TALLCMS_UPDATES_ENABLED', true),
// How often to check for updates (seconds). Default: 24 hours
'check_interval' => 86400,
// Cache TTL for GitHub API responses (seconds). Default: 1 hour
'cache_ttl' => 3600,
// GitHub repository for updates
'github_repo' => 'tallcms/tallcms',
// Optional GitHub token for higher API rate limits
'github_token' => env('TALLCMS_GITHUB_TOKEN'),
// Number of backup sets to retain
'backup_retention' => 3,
// Automatically backup files before updating
'auto_backup' => true,
// Require database backup before update
'require_db_backup' => true,
// Maximum database size for automatic backup (bytes). Default: 100MB
'db_backup_size_limit' => 100 * 1024 * 1024,
// Ed25519 public key for release signature verification (hex-encoded)
'public_key' => env('TALLCMS_UPDATE_PUBLIC_KEY', '6c41c964c60dd5341f7ba649dcda6e6de4b0b7afac2fbb9489527987907d35a9'),
],
];

8
config/theme.php Normal file
View File

@ -0,0 +1,8 @@
<?php
return [
'active' => env('TALLCMS_THEME_ACTIVE', 'talldaisy'),
'themes_path' => base_path('themes'),
'cache_themes' => env('TALLCMS_THEME_CACHE', true),
'auto_discover' => true,
];

View File

@ -0,0 +1,11 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('general.default_country_code', '+90');
}
};

1
public/themes/talldaisy Symbolic link
View File

@ -0,0 +1 @@
../../vendor/tallcms/cms/resources/themes/talldaisy/public