diff --git a/Modules/Admin/Filament/Pages/ManageGeneralSettings.php b/Modules/Admin/Filament/Pages/ManageGeneralSettings.php index 0c10a91ad..f478f3d37 100644 --- a/Modules/Admin/Filament/Pages/ManageGeneralSettings.php +++ b/Modules/Admin/Filament/Pages/ManageGeneralSettings.php @@ -7,7 +7,6 @@ use App\Support\CountryCodeManager; use App\Settings\GeneralSettings; use BackedEnum; use Filament\Forms\Components\FileUpload; -use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Select; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TextInput; @@ -15,6 +14,7 @@ use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Toggle; use Filament\Pages\SettingsPage; use Filament\Schemas\Schema; +use Modules\Admin\Support\HomeSlideFormSchema; use Tapp\FilamentCountryCodeField\Forms\Components\CountryCodeSelect; use UnitEnum; use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput; @@ -80,39 +80,10 @@ class ManageGeneralSettings extends SettingsPage ->default($defaults['site_description']) ->rows(3) ->maxLength(500), - Repeater::make('home_slides') - ->label('Ana Sayfa Slider') - ->schema([ - TextInput::make('badge') - ->label('Rozet') - ->required() - ->maxLength(255), - TextInput::make('title') - ->label('Başlık') - ->required() - ->maxLength(255), - Textarea::make('subtitle') - ->label('Alt Başlık') - ->rows(2) - ->required() - ->maxLength(500), - TextInput::make('primary_button_text') - ->label('Birincil Buton Metni') - ->required() - ->maxLength(120), - TextInput::make('secondary_button_text') - ->label('İkincil Buton Metni') - ->required() - ->maxLength(120), - ]) - ->default($defaults['home_slides']) - ->minItems(1) - ->collapsible() - ->reorderableWithButtons() - ->addActionLabel('Slide Ekle') - ->itemLabel(fn (array $state): ?string => filled($state['title'] ?? null) ? (string) $state['title'] : 'Slide') - ->afterStateHydrated(fn (Repeater $component, $state) => $component->state($this->normalizeHomeSlides($state))) - ->dehydrateStateUsing(fn ($state) => $this->normalizeHomeSlides($state)), + HomeSlideFormSchema::make( + $defaults['home_slides'], + fn ($state): array => $this->normalizeHomeSlides($state), + ), FileUpload::make('site_logo') ->label('Site Logosu') ->image() diff --git a/Modules/Admin/Filament/Pages/ManageHomeSlides.php b/Modules/Admin/Filament/Pages/ManageHomeSlides.php new file mode 100644 index 000000000..5290ce379 --- /dev/null +++ b/Modules/Admin/Filament/Pages/ManageHomeSlides.php @@ -0,0 +1,57 @@ + $this->normalizeHomeSlides($data['home_slides'] ?? $this->defaultHomeSlides()), + ]; + } + + public function form(Schema $schema): Schema + { + return $schema + ->components([ + HomeSlideFormSchema::make( + $this->defaultHomeSlides(), + fn ($state): array => $this->normalizeHomeSlides($state), + ), + ]); + } + + private function defaultHomeSlides(): array + { + return HomeSlideDefaults::defaults(); + } + + private function normalizeHomeSlides(mixed $state): array + { + return HomeSlideDefaults::normalize($state); + } +} diff --git a/Modules/Admin/Filament/Resources/CategoryResource.php b/Modules/Admin/Filament/Resources/CategoryResource.php index e8114ba3b..09d3c9f6f 100644 --- a/Modules/Admin/Filament/Resources/CategoryResource.php +++ b/Modules/Admin/Filament/Resources/CategoryResource.php @@ -40,12 +40,21 @@ class CategoryResource extends Resource { return $table->columns([ TextColumn::make('id')->sortable(), - TextColumn::make('name')->searchable()->sortable(), + TextColumn::make('name') + ->searchable() + ->formatStateUsing(fn (string $state, Category $record): string => $record->parent_id === null ? $state : '↳ ' . $state) + ->weight(fn (Category $record): string => $record->parent_id === null ? 'semi-bold' : 'normal'), TextColumn::make('parent.name')->label('Parent')->default('-'), - TextColumn::make('listings_count')->counts('listings')->label('Listings'), + TextColumn::make('children_count')->label('Subcategories'), + TextColumn::make('listings_count')->label('Listings'), IconColumn::make('is_active')->boolean(), TextColumn::make('sort_order')->sortable(), - ])->defaultSort('id', 'desc')->actions([ + ])->actions([ + Action::make('toggleChildren') + ->label(fn (Category $record, Pages\ListCategories $livewire): string => $livewire->hasExpandedChildren($record) ? 'Hide subcategories' : 'Show subcategories') + ->icon(fn (Category $record, Pages\ListCategories $livewire): string => $livewire->hasExpandedChildren($record) ? 'heroicon-o-chevron-down' : 'heroicon-o-chevron-right') + ->action(fn (Category $record, Pages\ListCategories $livewire) => $livewire->toggleChildren($record)) + ->visible(fn (Category $record): bool => $record->parent_id === null && $record->children_count > 0), EditAction::make(), Action::make('activities') ->icon('heroicon-o-clock') diff --git a/Modules/Admin/Filament/Resources/CategoryResource/Pages/ListCategories.php b/Modules/Admin/Filament/Resources/CategoryResource/Pages/ListCategories.php index fef019058..cb19046ff 100644 --- a/Modules/Admin/Filament/Resources/CategoryResource/Pages/ListCategories.php +++ b/Modules/Admin/Filament/Resources/CategoryResource/Pages/ListCategories.php @@ -3,10 +3,48 @@ namespace Modules\Admin\Filament\Resources\CategoryResource\Pages; use Filament\Actions\CreateAction; use Filament\Resources\Pages\ListRecords; +use Illuminate\Database\Eloquent\Builder; +use Livewire\Attributes\Url; use Modules\Admin\Filament\Resources\CategoryResource; +use Modules\Category\Models\Category; class ListCategories extends ListRecords { protected static string $resource = CategoryResource::class; - protected function getHeaderActions(): array { return [CreateAction::make()]; } + + #[Url(as: 'expanded')] + public array $expandedParents = []; + + protected function getHeaderActions(): array + { + return [CreateAction::make()]; + } + + public function toggleChildren(Category $record): void + { + if ($record->parent_id !== null || $record->children_count < 1) { + return; + } + + $recordId = (int) $record->getKey(); + + if (in_array($recordId, $this->expandedParents, true)) { + $this->expandedParents = array_values(array_diff($this->expandedParents, [$recordId])); + + return; + } + + $this->expandedParents[] = $recordId; + $this->expandedParents = array_values(array_unique(array_map('intval', $this->expandedParents))); + } + + public function hasExpandedChildren(Category $record): bool + { + return in_array((int) $record->getKey(), $this->expandedParents, true); + } + + protected function getTableQuery(): Builder + { + return Category::query()->forAdminHierarchy($this->expandedParents); + } } diff --git a/Modules/Admin/Filament/Resources/CityResource.php b/Modules/Admin/Filament/Resources/CityResource.php index f60eb0517..0d91e0e6a 100644 --- a/Modules/Admin/Filament/Resources/CityResource.php +++ b/Modules/Admin/Filament/Resources/CityResource.php @@ -15,6 +15,7 @@ use Filament\Tables\Columns\TextColumn; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; use Modules\Admin\Filament\Resources\CityResource\Pages; use Modules\Location\Models\City; use UnitEnum; @@ -23,7 +24,7 @@ class CityResource extends Resource { protected static ?string $model = City::class; protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-building-office-2'; - protected static string | UnitEnum | null $navigationGroup = 'Settings'; + protected static string | UnitEnum | null $navigationGroup = 'Location'; protected static ?string $label = 'City'; protected static ?string $pluralLabel = 'Cities'; protected static ?int $navigationSort = 3; @@ -52,6 +53,13 @@ class CityResource extends Resource ->relationship('country', 'name') ->searchable() ->preload(), + TernaryFilter::make('has_districts') + ->label('Has districts') + ->queries( + true: fn (Builder $query): Builder => $query->has('districts'), + false: fn (Builder $query): Builder => $query->doesntHave('districts'), + blank: fn (Builder $query): Builder => $query, + ), TernaryFilter::make('is_active')->label('Active'), ])->actions([ EditAction::make(), diff --git a/Modules/Admin/Filament/Resources/DistrictResource.php b/Modules/Admin/Filament/Resources/DistrictResource.php index f0ca451bb..872cd5613 100644 --- a/Modules/Admin/Filament/Resources/DistrictResource.php +++ b/Modules/Admin/Filament/Resources/DistrictResource.php @@ -25,7 +25,7 @@ class DistrictResource extends Resource { protected static ?string $model = District::class; protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-map'; - protected static string | UnitEnum | null $navigationGroup = 'Settings'; + protected static string | UnitEnum | null $navigationGroup = 'Location'; protected static ?string $label = 'District'; protected static ?string $pluralLabel = 'Districts'; protected static ?int $navigationSort = 4; diff --git a/Modules/Admin/Filament/Resources/LocationResource.php b/Modules/Admin/Filament/Resources/LocationResource.php index cdc679c20..23eb51e9e 100644 --- a/Modules/Admin/Filament/Resources/LocationResource.php +++ b/Modules/Admin/Filament/Resources/LocationResource.php @@ -11,8 +11,10 @@ use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Filters\TernaryFilter; use Filament\Tables\Table; +use Illuminate\Database\Eloquent\Builder; use Modules\Admin\Filament\Resources\LocationResource\Pages; use Modules\Location\Models\Country; use UnitEnum; @@ -21,7 +23,7 @@ class LocationResource extends Resource { protected static ?string $model = Country::class; protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-globe-alt'; - protected static string | UnitEnum | null $navigationGroup = 'Settings'; + protected static string | UnitEnum | null $navigationGroup = 'Location'; protected static ?string $label = 'Country'; protected static ?string $pluralLabel = 'Countries'; protected static ?int $navigationSort = 2; @@ -47,6 +49,16 @@ class LocationResource extends Resource IconColumn::make('is_active')->boolean(), TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true), ])->defaultSort('id', 'desc')->filters([ + SelectFilter::make('code') + ->label('Code') + ->options(fn (): array => Country::query()->orderBy('code')->pluck('code', 'code')->all()), + TernaryFilter::make('has_cities') + ->label('Has cities') + ->queries( + true: fn (Builder $query): Builder => $query->has('cities'), + false: fn (Builder $query): Builder => $query->doesntHave('cities'), + blank: fn (Builder $query): Builder => $query, + ), TernaryFilter::make('is_active')->label('Active'), ])->actions([ EditAction::make(), diff --git a/Modules/Admin/Providers/AdminPanelProvider.php b/Modules/Admin/Providers/AdminPanelProvider.php index 7dc59c0bd..779cd526a 100644 --- a/Modules/Admin/Providers/AdminPanelProvider.php +++ b/Modules/Admin/Providers/AdminPanelProvider.php @@ -7,6 +7,7 @@ use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; +use Filament\Navigation\MenuItem; use Filament\Pages\Dashboard; use Filament\Panel; use Filament\PanelProvider; @@ -24,7 +25,6 @@ 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 { @@ -39,10 +39,15 @@ class AdminPanelProvider extends PanelProvider ->discoverResources(in: module_path('Admin', 'Filament/Resources'), for: 'Modules\\Admin\\Filament\\Resources') ->discoverPages(in: module_path('Admin', 'Filament/Pages'), for: 'Modules\\Admin\\Filament\\Pages') ->discoverWidgets(in: module_path('Admin', 'Filament/Widgets'), for: 'Modules\\Admin\\Filament\\Widgets') + ->userMenuItems([ + 'view-site' => MenuItem::make() + ->label('View Site') + ->icon('heroicon-o-globe-alt') + ->url(fn (): string => url('/')) + ->sort(-2), + ]) ->plugins([ FilamentStateFusionPlugin::make(), - TallCmsPlugin::make() - ->withoutUsers(), BreezyCore::make() ->myProfile( shouldRegisterNavigation: true, diff --git a/Modules/Admin/Support/HomeSlideFormSchema.php b/Modules/Admin/Support/HomeSlideFormSchema.php new file mode 100644 index 000000000..49b354271 --- /dev/null +++ b/Modules/Admin/Support/HomeSlideFormSchema.php @@ -0,0 +1,63 @@ +label('Homepage Slides') + ->helperText('Use 1 to 5 slides. Upload a wide image for each slide to improve the hero area.') + ->schema([ + FileUpload::make('image_path') + ->label('Slide Image') + ->image() + ->disk('public') + ->directory('home-slides') + ->visibility('public') + ->imageEditor() + ->imagePreviewHeight('200') + ->helperText('Recommended: 1600x1000 or wider.') + ->columnSpanFull(), + TextInput::make('badge') + ->label('Badge') + ->maxLength(255), + TextInput::make('title') + ->label('Title') + ->required() + ->maxLength(255), + Textarea::make('subtitle') + ->label('Subtitle') + ->rows(3) + ->required() + ->maxLength(500) + ->columnSpanFull(), + TextInput::make('primary_button_text') + ->label('Primary Button') + ->required() + ->maxLength(120), + TextInput::make('secondary_button_text') + ->label('Secondary Button') + ->required() + ->maxLength(120), + ]) + ->columns(2) + ->default($defaults) + ->minItems(1) + ->maxItems(5) + ->collapsible() + ->collapsed() + ->cloneable() + ->reorderableWithButtons() + ->addActionLabel('Add Slide') + ->itemLabel(fn (array $state): string => filled($state['title'] ?? null) ? (string) $state['title'] : 'New Slide') + ->afterStateHydrated(fn (Repeater $component, $state) => $component->state($normalizeSlides($state))) + ->dehydrateStateUsing(fn ($state) => $normalizeSlides($state)); + } +} diff --git a/Modules/Category/Models/Category.php b/Modules/Category/Models/Category.php index 5f66ef1cf..55c092c0b 100644 --- a/Modules/Category/Models/Category.php +++ b/Modules/Category/Models/Category.php @@ -3,8 +3,8 @@ namespace Modules\Category\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Collection; use Modules\Listing\Models\Listing; use Spatie\Activitylog\LogOptions; @@ -50,6 +50,34 @@ class Category extends Model return $query->orderBy('sort_order')->orderBy('name'); } + public function scopeForAdminHierarchy(Builder $query, array $expandedParentIds = []): Builder + { + $expandedParentIds = collect($expandedParentIds) + ->map(fn ($id): int => (int) $id) + ->filter() + ->unique() + ->values() + ->all(); + + return $query + ->select('categories.*') + ->leftJoin('categories as parent_categories', 'categories.parent_id', '=', 'parent_categories.id') + ->with(['parent:id,name']) + ->withCount(['children', 'listings']) + ->where(function (Builder $nestedQuery) use ($expandedParentIds): void { + $nestedQuery->whereNull('categories.parent_id'); + + if ($expandedParentIds !== []) { + $nestedQuery->orWhereIn('categories.parent_id', $expandedParentIds); + } + }) + ->orderByRaw('COALESCE(parent_categories.sort_order, categories.sort_order)') + ->orderByRaw('COALESCE(parent_categories.name, categories.name)') + ->orderByRaw('CASE WHEN categories.parent_id IS NULL THEN 0 ELSE 1 END') + ->orderBy('categories.sort_order') + ->orderBy('categories.name'); + } + public static function filterOptions(): Collection { return static::query() diff --git a/Modules/Conversation/App/Http/Controllers/ConversationController.php b/Modules/Conversation/App/Http/Controllers/ConversationController.php index a1f860358..0b8150e83 100644 --- a/Modules/Conversation/App/Http/Controllers/ConversationController.php +++ b/Modules/Conversation/App/Http/Controllers/ConversationController.php @@ -16,13 +16,15 @@ class ConversationController extends Controller { public function inbox(Request $request): View { - $userId = (int) $request->user()->getKey(); + $user = $request->user(); + $userId = $user ? (int) $user->getKey() : null; + $requiresLogin = ! $user; $messageFilter = $this->resolveMessageFilter($request); $conversations = collect(); $selectedConversation = null; - if ($this->messagingTablesReady()) { + if ($userId && $this->messagingTablesReady()) { try { $conversations = Conversation::inboxForUser($userId, $messageFilter); $selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation')); @@ -50,6 +52,7 @@ class ConversationController extends Controller 'selectedConversation' => $selectedConversation, 'messageFilter' => $messageFilter, 'quickMessages' => QuickMessageCatalog::all(), + 'requiresLogin' => $requiresLogin, ]); } diff --git a/Modules/Conversation/resources/views/inbox.blade.php b/Modules/Conversation/resources/views/inbox.blade.php index adf679566..edf96e25c 100644 --- a/Modules/Conversation/resources/views/inbox.blade.php +++ b/Modules/Conversation/resources/views/inbox.blade.php @@ -8,6 +8,18 @@ @include('panel.partials.sidebar', ['activeMenu' => 'inbox'])
+ @if($requiresLogin ?? false) +
+
+

Inbox

+

Stay on this page and log in when you want to access your conversations.

+
+ + Log in + +
+ @endif +
diff --git a/Modules/Conversation/routes/web.php b/Modules/Conversation/routes/web.php index 1ff3c9478..7b1bc3a23 100644 --- a/Modules/Conversation/routes/web.php +++ b/Modules/Conversation/routes/web.php @@ -3,7 +3,7 @@ use Illuminate\Support\Facades\Route; use Modules\Conversation\App\Http\Controllers\ConversationController; -Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () { +Route::prefix('panel')->name('panel.')->group(function () { Route::get('/inbox', [ConversationController::class, 'inbox'])->name('inbox.index'); }); diff --git a/Modules/Favorite/App/Http/Controllers/FavoriteController.php b/Modules/Favorite/App/Http/Controllers/FavoriteController.php index 58fe0a9e6..74d0fcd5b 100644 --- a/Modules/Favorite/App/Http/Controllers/FavoriteController.php +++ b/Modules/Favorite/App/Http/Controllers/FavoriteController.php @@ -39,6 +39,7 @@ class FavoriteController extends Controller } $user = $request->user(); + $requiresLogin = ! $user; $categories = collect(); if ($this->tableExists('categories')) { @@ -55,7 +56,7 @@ class FavoriteController extends Controller $selectedConversation = null; $buyerConversationListingMap = []; - if ($activeTab === 'listings') { + if ($user && $activeTab === 'listings') { try { if ($this->tableExists('favorite_listings')) { $favoriteListings = $user->favoriteListings() @@ -100,7 +101,7 @@ class FavoriteController extends Controller } } - if ($activeTab === 'searches') { + if ($user && $activeTab === 'searches') { try { if ($this->tableExists('favorite_searches')) { $favoriteSearches = $user->favoriteSearches() @@ -114,7 +115,7 @@ class FavoriteController extends Controller } } - if ($activeTab === 'sellers') { + if ($user && $activeTab === 'sellers') { try { if ($this->tableExists('favorite_sellers')) { $favoriteSellers = $user->favoriteSellers() @@ -143,6 +144,7 @@ class FavoriteController extends Controller 'selectedConversation' => $selectedConversation, 'buyerConversationListingMap' => $buyerConversationListingMap, 'quickMessages' => QuickMessageCatalog::all(), + 'requiresLogin' => $requiresLogin, ]); } diff --git a/Modules/Favorite/resources/views/index.blade.php b/Modules/Favorite/resources/views/index.blade.php index 4e1b80fc0..c96e41efa 100644 --- a/Modules/Favorite/resources/views/index.blade.php +++ b/Modules/Favorite/resources/views/index.blade.php @@ -8,6 +8,18 @@ @include('panel.partials.sidebar', ['activeMenu' => 'favorites', 'activeFavoritesTab' => $activeTab])
+ @if($requiresLogin ?? false) +
+
+

Favorites

+

Stay on this page and log in when you want to sync saved listings, searches, and sellers.

+
+ + Log in + +
+ @endif + @if($activeTab === 'listings') @php $listingTabQuery = array_filter([ diff --git a/Modules/Favorite/routes/web.php b/Modules/Favorite/routes/web.php index c29819b53..12afcf09b 100644 --- a/Modules/Favorite/routes/web.php +++ b/Modules/Favorite/routes/web.php @@ -3,8 +3,11 @@ use Illuminate\Support\Facades\Route; use Modules\Favorite\App\Http\Controllers\FavoriteController; -Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () { +Route::prefix('favorites')->name('favorites.')->group(function () { Route::get('/', [FavoriteController::class, 'index'])->name('index'); +}); + +Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () { Route::post('/listings/{listing}/toggle', [FavoriteController::class, 'toggleListing'])->name('listings.toggle'); Route::post('/sellers/{seller}/toggle', [FavoriteController::class, 'toggleSeller'])->name('sellers.toggle'); Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store'); diff --git a/Modules/Listing/Http/Controllers/ListingController.php b/Modules/Listing/Http/Controllers/ListingController.php index 7186c4751..acc1fc67c 100644 --- a/Modules/Listing/Http/Controllers/ListingController.php +++ b/Modules/Listing/Http/Controllers/ListingController.php @@ -4,7 +4,6 @@ namespace Modules\Listing\Http\Controllers; use App\Http\Controllers\Controller; use Modules\Conversation\App\Models\Conversation; use Modules\Favorite\App\Models\FavoriteSearch; -use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Schema; use Modules\Location\Models\City; @@ -67,18 +66,28 @@ class ListingController extends Controller $listingDirectory = Category::listingDirectory($categoryId); + $browseFilters = [ + 'search' => $search, + 'country' => $selectedCountryName, + 'city' => $selectedCityName, + 'min_price' => $minPrice, + 'max_price' => $maxPrice, + 'date_filter' => $dateFilter, + ]; + + $allListingsTotal = Listing::query() + ->active() + ->forBrowseFilters($browseFilters) + ->count(); + $listingsQuery = Listing::query() ->active() ->with('category:id,name') - ->searchTerm($search) - ->forCategoryIds($listingDirectory['filterIds']) - ->when($selectedCountryName, fn ($query) => $query->where('country', $selectedCountryName)) - ->when($selectedCityName, fn ($query) => $query->where('city', $selectedCityName)) - ->when(! is_null($minPrice), fn ($query) => $query->whereNotNull('price')->where('price', '>=', $minPrice)) - ->when(! is_null($maxPrice), fn ($query) => $query->whereNotNull('price')->where('price', '<=', $maxPrice)); - - $this->applyDateFilter($listingsQuery, $dateFilter); - $this->applySorting($listingsQuery, $sort); + ->forBrowseFilters([ + ...$browseFilters, + 'category_ids' => $listingDirectory['filterIds'], + ]) + ->applyBrowseSort($sort); $listings = $listingsQuery ->paginate(16) @@ -136,6 +145,7 @@ class ListingController extends Controller 'favoriteListingIds', 'isCurrentSearchSaved', 'conversationListingMap', + 'allListingsTotal', )); } @@ -302,24 +312,4 @@ class ListingController extends Controller } } - private function applyDateFilter($query, string $dateFilter): void - { - match ($dateFilter) { - 'today' => $query->where('created_at', '>=', Carbon::now()->startOfDay()), - 'week' => $query->where('created_at', '>=', Carbon::now()->subDays(7)), - 'month' => $query->where('created_at', '>=', Carbon::now()->subDays(30)), - default => null, - }; - } - - private function applySorting($query, string $sort): void - { - match ($sort) { - 'newest' => $query->reorder()->orderByDesc('created_at'), - 'oldest' => $query->reorder()->orderBy('created_at'), - 'price_asc' => $query->reorder()->orderByRaw('price is null')->orderBy('price'), - 'price_desc' => $query->reorder()->orderByRaw('price is null')->orderByDesc('price'), - default => $query->reorder()->orderByDesc('is_featured')->orderByDesc('created_at'), - }; - } } diff --git a/Modules/Listing/Models/Listing.php b/Modules/Listing/Models/Listing.php index 50cfab50d..3c7c24fa5 100644 --- a/Modules/Listing/Models/Listing.php +++ b/Modules/Listing/Models/Listing.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Modules\Category\Models\Category; @@ -118,6 +119,43 @@ class Listing extends Model implements HasMedia return $query->whereIn('category_id', $categoryIds); } + public function scopeForBrowseFilters(Builder $query, array $filters): Builder + { + $search = trim((string) ($filters['search'] ?? '')); + $country = isset($filters['country']) ? trim((string) $filters['country']) : null; + $city = isset($filters['city']) ? trim((string) $filters['city']) : null; + $minPrice = is_numeric($filters['min_price'] ?? null) ? max((float) $filters['min_price'], 0) : null; + $maxPrice = is_numeric($filters['max_price'] ?? null) ? max((float) $filters['max_price'], 0) : null; + $dateFilter = (string) ($filters['date_filter'] ?? 'all'); + $categoryIds = $filters['category_ids'] ?? null; + + $query + ->searchTerm($search) + ->forCategoryIds(is_array($categoryIds) ? $categoryIds : null) + ->when($country !== null && $country !== '', fn (Builder $builder) => $builder->where('country', $country)) + ->when($city !== null && $city !== '', fn (Builder $builder) => $builder->where('city', $city)) + ->when(! is_null($minPrice), fn (Builder $builder) => $builder->whereNotNull('price')->where('price', '>=', $minPrice)) + ->when(! is_null($maxPrice), fn (Builder $builder) => $builder->whereNotNull('price')->where('price', '<=', $maxPrice)); + + return match ($dateFilter) { + 'today' => $query->where('created_at', '>=', Carbon::now()->startOfDay()), + 'week' => $query->where('created_at', '>=', Carbon::now()->subDays(7)), + 'month' => $query->where('created_at', '>=', Carbon::now()->subDays(30)), + default => $query, + }; + } + + public function scopeApplyBrowseSort(Builder $query, string $sort): Builder + { + return match ($sort) { + 'newest' => $query->reorder()->orderByDesc('created_at'), + 'oldest' => $query->reorder()->orderBy('created_at'), + 'price_asc' => $query->reorder()->orderByRaw('price is null')->orderBy('price'), + 'price_desc' => $query->reorder()->orderByRaw('price is null')->orderByDesc('price'), + default => $query->reorder()->orderByDesc('is_featured')->orderByDesc('created_at'), + }; + } + public function themeGallery(): array { $mediaUrls = $this->getMedia('listing-images') diff --git a/Modules/Listing/resources/views/index.blade.php b/Modules/Listing/resources/views/index.blade.php index a5c4b361a..bf13efa86 100644 --- a/Modules/Listing/resources/views/index.blade.php +++ b/Modules/Listing/resources/views/index.blade.php @@ -1,7 +1,7 @@ @extends('app::layouts.app') @section('content') @php - $totalListings = (int) $listings->total(); + $totalListings = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total(); $activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : ''; $pageTitle = $activeCategoryName !== '' ? 'İkinci El '.$activeCategoryName.' İlanları ve Fiyatları' diff --git a/Modules/Listing/resources/views/themes/default/index.blade.php b/Modules/Listing/resources/views/themes/default/index.blade.php index a5c4b361a..bf13efa86 100644 --- a/Modules/Listing/resources/views/themes/default/index.blade.php +++ b/Modules/Listing/resources/views/themes/default/index.blade.php @@ -1,7 +1,7 @@ @extends('app::layouts.app') @section('content') @php - $totalListings = (int) $listings->total(); + $totalListings = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total(); $activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : ''; $pageTitle = $activeCategoryName !== '' ? 'İkinci El '.$activeCategoryName.' İlanları ve Fiyatları' diff --git a/Modules/Listing/resources/views/themes/otoplus/index.blade.php b/Modules/Listing/resources/views/themes/otoplus/index.blade.php index a5c4b361a..bf13efa86 100644 --- a/Modules/Listing/resources/views/themes/otoplus/index.blade.php +++ b/Modules/Listing/resources/views/themes/otoplus/index.blade.php @@ -1,7 +1,7 @@ @extends('app::layouts.app') @section('content') @php - $totalListings = (int) $listings->total(); + $totalListings = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total(); $activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : ''; $pageTitle = $activeCategoryName !== '' ? 'İkinci El '.$activeCategoryName.' İlanları ve Fiyatları' diff --git a/Modules/User/App/Models/User.php b/Modules/User/App/Models/User.php index 705299105..242093dc9 100644 --- a/Modules/User/App/Models/User.php +++ b/Modules/User/App/Models/User.php @@ -135,6 +135,16 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata return filled($this->avatar_url) ? Storage::disk('public')->url($this->avatar_url) : null; } + public function getDisplayName(): string + { + return trim((string) ($this->name ?: $this->email ?: 'User')); + } + + public function getEmail(): string + { + return trim((string) $this->email); + } + public function toggleFavoriteListing(Listing $listing): bool { $isFavorite = $this->favoriteListings()->whereKey($listing->getKey())->exists(); diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index 613bcd9d9..7ccfe0708 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -11,19 +11,27 @@ use Illuminate\View\View; class AuthenticatedSessionController extends Controller { - /** - * Display the login view. - */ public function create(): View { - return view('auth.login'); + $redirectTo = $this->sanitizeRedirectTarget(request()->query('redirect')); + + if ($redirectTo) { + request()->session()->put('url.intended', $redirectTo); + } + + return view('auth.login', [ + 'redirectTo' => $redirectTo, + ]); } - /** - * Handle an incoming authentication request. - */ public function store(LoginRequest $request): RedirectResponse { + $redirectTo = $this->sanitizeRedirectTarget($request->input('redirect')); + + if ($redirectTo) { + $request->session()->put('url.intended', $redirectTo); + } + $request->authenticate(); $request->session()->regenerate(); @@ -31,9 +39,6 @@ class AuthenticatedSessionController extends Controller return redirect()->intended(route('dashboard', absolute: false)); } - /** - * Destroy an authenticated session. - */ public function destroy(Request $request): RedirectResponse { Auth::guard('web')->logout(); @@ -44,4 +49,34 @@ class AuthenticatedSessionController extends Controller return redirect('/'); } + + private function sanitizeRedirectTarget(?string $target): ?string + { + $target = trim((string) $target); + + if ($target === '' || str_starts_with($target, '//')) { + return null; + } + + if (str_starts_with($target, '/')) { + return $target; + } + + if (! filter_var($target, FILTER_VALIDATE_URL)) { + return null; + } + + $applicationUrl = parse_url(url('/')); + $targetUrl = parse_url($target); + + if (($applicationUrl['host'] ?? null) !== ($targetUrl['host'] ?? null)) { + return null; + } + + $path = $targetUrl['path'] ?? '/'; + $query = isset($targetUrl['query']) ? '?' . $targetUrl['query'] : ''; + $fragment = isset($targetUrl['fragment']) ? '#' . $targetUrl['fragment'] : ''; + + return $path . $query . $fragment; + } } diff --git a/app/Support/HomeSlideDefaults.php b/app/Support/HomeSlideDefaults.php index cb35c91f8..a2f0c6b0b 100644 --- a/app/Support/HomeSlideDefaults.php +++ b/app/Support/HomeSlideDefaults.php @@ -2,40 +2,45 @@ namespace App\Support; +use Illuminate\Support\Arr; + final class HomeSlideDefaults { /** - * @return array + * @return array */ public static function defaults(): array { return [ [ - 'badge' => 'Vitrin İlanları', - 'title' => 'İlan ücreti ödemeden ürününü dakikalar içinde yayına al.', - 'subtitle' => 'Mahallendeki alıcılarla hızlıca buluş, pazarlığı doğrudan mesajla tamamla.', - 'primary_button_text' => 'İlanları İncele', - 'secondary_button_text' => 'İlan Ver', + 'badge' => 'Featured Marketplace', + 'title' => 'List products in minutes and reach local buyers faster.', + 'subtitle' => 'A calm, simple marketplace for everyday electronics, home finds, and local deals.', + 'primary_button_text' => 'Browse Listings', + 'secondary_button_text' => 'Post Listing', + 'image_path' => 'images/home-slides/slide-marketplace.svg', ], [ - 'badge' => 'Günün Fırsatları', - 'title' => 'Elektronikten araca kadar her kategoride canlı ilanlar seni bekliyor.', - 'subtitle' => 'Kategorilere göz at, favorilerine ekle ve satıcılarla tek tıkla iletişime geç.', - 'primary_button_text' => 'Kategorileri Gör', - 'secondary_button_text' => 'Hemen Başla', + 'badge' => 'Fresh Categories', + 'title' => 'Explore electronics, vehicles, fashion, and home in one clean flow.', + 'subtitle' => 'Move between categories quickly, compare listings, and message sellers without friction.', + 'primary_button_text' => 'See Categories', + 'secondary_button_text' => 'Start Now', + 'image_path' => 'images/home-slides/slide-categories.svg', ], [ - 'badge' => 'Yerel Alışveriş', - 'title' => 'Konumuna en yakın ikinci el fırsatları tek ekranda keşfet.', - 'subtitle' => 'Şehrini seç, sana en yakın ilanları filtrele ve güvenle alışveriş yap.', - 'primary_button_text' => 'Yakındaki İlanlar', - 'secondary_button_text' => 'Ücretsiz İlan Ver', + 'badge' => 'Local Shopping', + 'title' => 'Discover nearby second-hand picks with a more polished storefront.', + 'subtitle' => 'Filter by city, save favorites, and turn local demand into quick conversations.', + 'primary_button_text' => 'Nearby Deals', + 'secondary_button_text' => 'Sell for Free', + 'image_path' => 'images/home-slides/slide-local.svg', ], ]; } /** - * @return array + * @return array */ public static function normalize(mixed $slides): array { @@ -52,6 +57,7 @@ final class HomeSlideDefaults $subtitle = trim((string) ($slide['subtitle'] ?? '')); $primaryButtonText = trim((string) ($slide['primary_button_text'] ?? '')); $secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? '')); + $imagePath = self::normalizeImagePath($slide['image_path'] ?? null); if ($title === '') { return null; @@ -63,6 +69,7 @@ final class HomeSlideDefaults 'subtitle' => $subtitle !== '' ? $subtitle : $fallback['subtitle'], 'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : $fallback['primary_button_text'], 'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : $fallback['secondary_button_text'], + 'image_path' => $imagePath !== '' ? $imagePath : ($fallback['image_path'] ?? null), ]; }) ->filter(fn ($slide): bool => is_array($slide)) @@ -74,4 +81,19 @@ final class HomeSlideDefaults ->values() ->all(); } + + private static function normalizeImagePath(mixed $value): string + { + if (is_string($value)) { + return trim($value); + } + + if (is_array($value)) { + $firstValue = Arr::first($value, fn ($item): bool => is_string($item) && trim($item) !== ''); + + return is_string($firstValue) ? trim($firstValue) : ''; + } + + return ''; + } } diff --git a/composer.json b/composer.json index ed6bb996a..3898d959e 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,6 @@ "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" }, diff --git a/config/tallcms.php b/config/tallcms.php deleted file mode 100644 index 0f45d0b70..000000000 --- a/config/tallcms.php +++ /dev/null @@ -1,569 +0,0 @@ - (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'), - ], -]; diff --git a/database/seeders/HomeSliderSettingsSeeder.php b/database/seeders/HomeSliderSettingsSeeder.php index 56a32d5b9..26c56eb5c 100644 --- a/database/seeders/HomeSliderSettingsSeeder.php +++ b/database/seeders/HomeSliderSettingsSeeder.php @@ -11,13 +11,8 @@ class HomeSliderSettingsSeeder extends Seeder public function run(): void { $settings = app(GeneralSettings::class); - $settings->home_slides = HomeSlideDefaults::normalize($settings->home_slides ?? []); + $settings->home_slides = HomeSlideDefaults::defaults(); $settings->save(); } - - private function defaultHomeSlides(): array - { - return HomeSlideDefaults::defaults(); - } } diff --git a/lang/tr/messages.php b/lang/tr/messages.php index 71f79ff24..13ceabee7 100644 --- a/lang/tr/messages.php +++ b/lang/tr/messages.php @@ -1,22 +1,22 @@ 'OpenClassify', - 'home' => 'Ana Sayfa', - 'categories' => 'Kategoriler', - 'listings' => 'İlanlar', - 'search' => 'Ara', - 'search_placeholder' => 'Her şeyi arayın...', - 'login' => 'Giriş', - 'register' => 'Kayıt Ol', - 'logout' => 'Çıkış', - 'find_what_you_need' => 'İhtiyacınızı Bulun', - 'hero_subtitle' => 'Bölgenizdeki her şeyi alın ve satın', - 'browse_categories' => 'Kategorilere Göz At', - 'recent_listings' => 'Son İlanlar', - 'featured_listings' => 'Öne Çıkan İlanlar', - 'post_listing' => 'İlan Ver', - 'sell_something' => 'Satılık bir şeyiniz mi var?', - 'free' => 'Ücretsiz', - 'view' => 'Görüntüle', - 'contact_seller' => 'Satıcıyla İletişim', + 'home' => 'Home', + 'categories' => 'Categories', + 'listings' => 'Listings', + 'search' => 'Search', + 'search_placeholder' => 'Search for anything...', + 'login' => 'Login', + 'register' => 'Register', + 'logout' => 'Logout', + 'find_what_you_need' => 'Find What You Need', + 'hero_subtitle' => 'Buy and sell everything in your area', + 'browse_categories' => 'Browse Categories', + 'recent_listings' => 'Recent Listings', + 'featured_listings' => 'Featured Listings', + 'post_listing' => 'Post Listing', + 'sell_something' => 'Have something to sell?', + 'free' => 'Free', + 'view' => 'View', + 'contact_seller' => 'Contact Seller', ]; diff --git a/public/images/home-slides/slide-categories.svg b/public/images/home-slides/slide-categories.svg new file mode 100644 index 000000000..396090455 --- /dev/null +++ b/public/images/home-slides/slide-categories.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/home-slides/slide-local.svg b/public/images/home-slides/slide-local.svg new file mode 100644 index 000000000..40a053302 --- /dev/null +++ b/public/images/home-slides/slide-local.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/home-slides/slide-marketplace.svg b/public/images/home-slides/slide-marketplace.svg new file mode 100644 index 000000000..e8e160179 --- /dev/null +++ b/public/images/home-slides/slide-marketplace.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/css/app.css b/resources/css/app.css index 9018d2eb2..7767e0e5f 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -84,6 +84,178 @@ h6 { border-bottom: 1px solid var(--oc-border); } +.oc-nav-wrap { + max-width: 1320px; + margin: 0 auto; + padding: 18px 16px 14px; +} + +.oc-nav-main { + display: grid; + grid-template-columns: auto minmax(320px, 1fr) auto; + align-items: center; + gap: 18px; +} + +.oc-brand { + display: inline-flex; + align-items: center; + gap: 14px; + min-width: 0; +} + +.oc-search { + display: flex; + align-items: center; + gap: 12px; + min-height: 56px; + padding: 0 14px 0 18px; + border: 1px solid rgba(29, 29, 31, 0.08); + border-radius: 999px; + background: rgba(255, 255, 255, 0.92); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); +} + +.oc-search-icon { + color: #6e6e73; + flex-shrink: 0; +} + +.oc-search-input { + width: 100%; + border: 0; + background: transparent; + color: var(--oc-text); + font-size: 1rem; + line-height: 1.4; + outline: none; +} + +.oc-search-input::placeholder { + color: #8d8d92; +} + +.oc-search-submit { + border: 0; + background: transparent; + color: #4b5563; + font-size: 0.95rem; + font-weight: 600; + white-space: nowrap; + cursor: pointer; +} + +.oc-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 12px; +} + +.oc-pill { + display: inline-flex; + align-items: center; + gap: 10px; + min-height: 48px; + padding: 0 18px; + border: 1px solid rgba(29, 29, 31, 0.08); + border-radius: 999px; + background: rgba(255, 255, 255, 0.84); + color: #4b5563; + font-size: 0.95rem; + font-weight: 500; +} + +.oc-pill-strong { + color: #fff; + background: linear-gradient(180deg, #2997ff, var(--oc-primary)); + border-color: transparent; + box-shadow: 0 10px 22px rgba(0, 113, 227, 0.18); +} + +.oc-cta { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 48px; + padding: 0 22px; + font-size: 0.96rem; + font-weight: 600; +} + +.oc-text-link { + color: #6e6e73; + font-size: 0.95rem; + font-weight: 500; + transition: color 0.2s ease; +} + +.oc-text-link:hover { + color: var(--oc-text); +} + +.oc-mobile-tools { + display: grid; + gap: 12px; + margin-top: 14px; +} + +.oc-mobile-pills { + display: flex; + gap: 10px; + overflow-x: auto; + padding-bottom: 2px; +} + +.oc-category-row { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid rgba(29, 29, 31, 0.08); +} + +.oc-category-track { + display: flex; + align-items: center; + gap: 8px; + overflow-x: auto; + padding-bottom: 2px; +} + +.oc-category-pill { + display: inline-flex; + align-items: center; + gap: 10px; + min-height: 46px; + padding: 0 18px; + border-radius: 999px; + border: 1px solid rgba(29, 29, 31, 0.08); + background: rgba(255, 255, 255, 0.84); + color: var(--oc-text); + font-size: 0.95rem; + font-weight: 600; + white-space: nowrap; +} + +.oc-category-link { + display: inline-flex; + align-items: center; + min-height: 46px; + padding: 0 18px; + border-radius: 999px; + color: #4b5563; + font-size: 0.95rem; + font-weight: 500; + white-space: nowrap; + transition: background 0.2s ease, color 0.2s ease; +} + +.oc-category-link:hover, +.oc-category-pill:hover, +.oc-pill:hover { + background: rgba(255, 255, 255, 0.98); + color: var(--oc-text); +} + .search-shell { border: 1px solid var(--oc-border); background: #ffffff; @@ -140,6 +312,58 @@ h6 { font-size: 0.875rem; } +@media (max-width: 1279px) { + .oc-nav-main { + grid-template-columns: auto minmax(0, 1fr); + } + + .oc-actions { + grid-column: 1 / -1; + justify-content: space-between; + flex-wrap: wrap; + } +} + +@media (max-width: 1023px) { + .oc-nav-wrap { + padding-top: 14px; + padding-bottom: 12px; + } + + .oc-nav-main { + grid-template-columns: 1fr auto; + gap: 12px; + } + + .brand-text { + font-size: 1.42rem; + } + + .oc-actions { + grid-column: auto; + justify-content: flex-end; + } +} + +@media (max-width: 767px) { + .oc-nav-main { + grid-template-columns: 1fr; + } + + .oc-brand { + justify-content: center; + } + + .oc-actions { + display: none; + } + + .oc-category-row { + margin-top: 12px; + padding-top: 12px; + } +} + summary::-webkit-details-marker { display: none; } diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 278082297..d0961a57c 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -15,6 +15,13 @@
@csrf + @php + $redirectInput = old('redirect', $redirectTo ?? request('redirect')); + @endphp + + @if(filled($redirectInput)) + + @endif
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 616e53ace..4572f4b66 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -13,13 +13,21 @@ $subtitle = trim((string) ($slide['subtitle'] ?? '')); $primaryButtonText = trim((string) ($slide['primary_button_text'] ?? '')); $secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? '')); + $imagePath = trim((string) ($slide['image_path'] ?? '')); return [ 'badge' => $badge !== '' ? $badge : 'OpenClassify Marketplace', - 'title' => $title !== '' ? $title : 'İlan ücreti ödemeden ürününü hızla sat!', + 'title' => $title !== '' ? $title : 'Sell faster with a cleaner local marketplace.', 'subtitle' => $subtitle !== '' ? $subtitle : 'Buy and sell everything in your area', - 'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : 'İncele', + 'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : 'Browse Listings', 'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : 'Post Listing', + 'image_url' => $imagePath !== '' + ? (str_starts_with($imagePath, 'http://') || str_starts_with($imagePath, 'https://') + ? $imagePath + : (str_starts_with($imagePath, 'images/') + ? asset($imagePath) + : \Illuminate\Support\Facades\Storage::disk('public')->url($imagePath))) + : null, ]; }) ->values(); @@ -28,10 +36,11 @@ $homeSlides = collect([ [ 'badge' => 'OpenClassify Marketplace', - 'title' => 'İlan ücreti ödemeden ürününü hızla sat!', + 'title' => 'Sell faster with a cleaner local marketplace.', 'subtitle' => 'Buy and sell everything in your area', - 'primary_button_text' => 'İncele', + 'primary_button_text' => 'Browse Listings', 'secondary_button_text' => 'Post Listing', + 'image_url' => null, ], ]); } @@ -134,7 +143,7 @@

OpenClassify

-

Ürün, kategori, satıcı ara

+

Search listings, categories, and sellers

@@ -150,15 +159,25 @@
-
- @if($heroImage) - {{ $heroListing?->title }} - @else -
- 🚗 -

Görsel eklendiğinde burada öne çıkan ilan yer alacak.

+
+ @foreach($homeSlides as $index => $slide) +
$index !== 0]) + aria-hidden="{{ $index === 0 ? 'false' : 'true' }}" + > + @if($slide['image_url']) + {{ $slide['title'] }} + @elseif($heroImage) + {{ $heroListing?->title }} + @else +
+ +

Upload a slide image to make this area feel complete.

+
+ @endif
- @endif + @endforeach
@@ -375,6 +394,7 @@ } const slides = Array.from(slider.querySelectorAll('[data-home-slide]')); + const visuals = Array.from(document.querySelectorAll('[data-home-slide-visual]')); const dots = Array.from(slider.querySelectorAll('[data-home-slide-dot]')); const previousButton = slider.querySelector('[data-home-slide-prev]'); const nextButton = slider.querySelector('[data-home-slide-next]'); @@ -396,6 +416,13 @@ slide.setAttribute('aria-hidden', isActive ? 'false' : 'true'); }); + visuals.forEach((visual, visualIndex) => { + const isActive = visualIndex === activeIndex; + + visual.classList.toggle('hidden', !isActive); + visual.setAttribute('aria-hidden', isActive ? 'false' : 'true'); + }); + dots.forEach((dot, dotIndex) => { const isActive = dotIndex === activeIndex; diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 2840e215d..d81d5664c 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -46,19 +46,19 @@