From 6ee6da3d83429190a9f937e0363add638c133ef7 Mon Sep 17 00:00:00 2001 From: fatihalp Date: Sat, 7 Mar 2026 16:37:03 +0300 Subject: [PATCH] Refactor modules UI and seeders --- Modules/Admin/Support/HomeSlideFormSchema.php | 1 - Modules/Category/Models/Category.php | 10 + .../Http/Controllers/FavoriteController.php | 38 +- .../Favorite/resources/views/index.blade.php | 177 --------- .../Seeders/ListingCustomFieldSeeder.php | 28 ++ Modules/Listing/Models/ListingCustomField.php | 19 + .../Support/ListingCustomFieldSeedCatalog.php | 296 ++++++++++++++ app/Livewire/PanelQuickListingForm.php | 22 +- database/seeders/DatabaseSeeder.php | 1 + resources/css/app.css | 367 +++++++++++++++--- resources/views/layouts/app.blade.php | 214 ++++++++-- resources/views/panel/create.blade.php | 2 +- .../partials/quick-create/form.blade.php | 95 ++++- 13 files changed, 957 insertions(+), 313 deletions(-) create mode 100644 Modules/Listing/Database/Seeders/ListingCustomFieldSeeder.php create mode 100644 Modules/Listing/Support/ListingCustomFieldSeedCatalog.php diff --git a/Modules/Admin/Support/HomeSlideFormSchema.php b/Modules/Admin/Support/HomeSlideFormSchema.php index 49b354271..3ae1b5f42 100644 --- a/Modules/Admin/Support/HomeSlideFormSchema.php +++ b/Modules/Admin/Support/HomeSlideFormSchema.php @@ -57,7 +57,6 @@ final class HomeSlideFormSchema ->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 55c092c0b..24d3e9337 100644 --- a/Modules/Category/Models/Category.php +++ b/Modules/Category/Models/Category.php @@ -95,6 +95,16 @@ class Category extends Model ->get(['id', 'name', 'slug']); } + public static function seedableListingFieldCategories(): Collection + { + return static::query() + ->active() + ->with('parent:id,name,slug,parent_id') + ->ordered() + ->get() + ->values(); + } + public static function rootTreeWithActiveChildren(): Collection { return static::query() diff --git a/Modules/Favorite/App/Http/Controllers/FavoriteController.php b/Modules/Favorite/App/Http/Controllers/FavoriteController.php index 74d0fcd5b..e140cc3e2 100644 --- a/Modules/Favorite/App/Http/Controllers/FavoriteController.php +++ b/Modules/Favorite/App/Http/Controllers/FavoriteController.php @@ -8,7 +8,6 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Schema; use Modules\Category\Models\Category; use Modules\Conversation\App\Models\Conversation; -use Modules\Conversation\App\Support\QuickMessageCatalog; use Modules\Favorite\App\Models\FavoriteSearch; use Modules\Listing\Models\Listing; use Modules\User\App\Models\User; @@ -28,11 +27,6 @@ class FavoriteController extends Controller $statusFilter = 'all'; } - $messageFilter = (string) $request->string('message_filter', 'all'); - if (! in_array($messageFilter, ['all', 'unread', 'important'], true)) { - $messageFilter = 'all'; - } - $selectedCategoryId = $request->integer('category'); if ($selectedCategoryId <= 0) { $selectedCategoryId = null; @@ -52,8 +46,6 @@ class FavoriteController extends Controller $favoriteListings = $this->emptyPaginator(); $favoriteSearches = $this->emptyPaginator(); $favoriteSellers = $this->emptyPaginator(); - $conversations = collect(); - $selectedConversation = null; $buyerConversationListingMap = []; if ($user && $activeTab === 'listings') { @@ -69,34 +61,20 @@ class FavoriteController extends Controller ->withQueryString(); } - if ($this->tableExists('conversations') && $this->tableExists('conversation_messages')) { + if ( + $favoriteListings->isNotEmpty() + && $this->tableExists('conversations') + ) { $userId = (int) $user->getKey(); - $conversations = Conversation::inboxForUser($userId, $messageFilter); - $buyerConversationListingMap = $conversations + $buyerConversationListingMap = Conversation::query() ->where('buyer_id', $userId) + ->whereIn('listing_id', $favoriteListings->pluck('id')->all()) ->pluck('id', 'listing_id') ->map(fn ($conversationId) => (int) $conversationId) ->all(); - - $selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation')); - - if ($selectedConversation) { - $selectedConversation->loadThread(); - $selectedConversation->markAsReadFor($userId); - - $conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation { - if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) { - $conversation->unread_count = 0; - } - - return $conversation; - }); - } } } catch (Throwable) { $favoriteListings = $this->emptyPaginator(); - $conversations = collect(); - $selectedConversation = null; $buyerConversationListingMap = []; } } @@ -135,15 +113,11 @@ class FavoriteController extends Controller 'activeTab' => $activeTab, 'statusFilter' => $statusFilter, 'selectedCategoryId' => $selectedCategoryId, - 'messageFilter' => $messageFilter, 'categories' => $categories, 'favoriteListings' => $favoriteListings, 'favoriteSearches' => $favoriteSearches, 'favoriteSellers' => $favoriteSellers, - 'conversations' => $conversations, - '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 c96e41efa..63b3a2ce6 100644 --- a/Modules/Favorite/resources/views/index.blade.php +++ b/Modules/Favorite/resources/views/index.blade.php @@ -26,7 +26,6 @@ 'tab' => 'listings', 'status' => $statusFilter, 'category' => $selectedCategoryId, - 'message_filter' => $messageFilter, ], fn ($value) => !is_null($value) && $value !== ''); @endphp
@@ -42,7 +41,6 @@
- - @if($selectedCategoryId) - - @endif - @@ -145,176 +138,6 @@ @if($favoriteListings?->hasPages())
{{ $favoriteListings->links() }}
@endif - -
-
-
-
-
-

Gelen Kutusu

- - - -
-
-

Hızlı Filtreler

- -
-
- @forelse($conversations as $conversation) - @php - $conversationListing = $conversation->listing; - $partner = (int) $conversation->buyer_id === (int) auth()->id() ? $conversation->seller : $conversation->buyer; - $isSelected = $selectedConversation && (int) $selectedConversation->id === (int) $conversation->id; - $conversationImage = $conversationListing?->getFirstMediaUrl('listing-images'); - $lastMessage = trim((string) ($conversation->lastMessage?->body ?? '')); - @endphp - -
-
- @if($conversationImage) - {{ $conversationListing?->title }} - @else -
İlan
- @endif -
-
-
-

{{ $partner?->name ?? 'Kullanıcı' }}

-

{{ $conversation->last_message_at?->format('d.m.Y') }}

-
-

{{ $conversationListing?->title ?? 'İlan silinmiş' }}

-

- {{ $lastMessage !== '' ? $lastMessage : 'Henüz mesaj yok' }} -

-
- @if($conversation->unread_count > 0) - - {{ $conversation->unread_count }} - - @endif -
-
- @empty -
- Henüz bir sohbetin yok. -
- @endforelse -
-
- -
- @if($selectedConversation) - @php - $activeListing = $selectedConversation->listing; - $activePartner = (int) $selectedConversation->buyer_id === (int) auth()->id() - ? $selectedConversation->seller - : $selectedConversation->buyer; - $activePriceLabel = $activeListing && !is_null($activeListing->price) - ? number_format((float) $activeListing->price, 0).' '.($activeListing->currency ?? 'TL') - : null; - @endphp -
-
- {{ strtoupper(substr((string) ($activePartner?->name ?? 'K'), 0, 1)) }} -
-
-

{{ $activePartner?->name ?? 'Kullanıcı' }}

-

{{ $activeListing?->title ?? 'İlan silinmiş' }}

-
- @if($activePriceLabel) -
{{ $activePriceLabel }}
- @endif -
- -
- @forelse($selectedConversation->messages as $message) - @php $isMine = (int) $message->sender_id === (int) auth()->id(); @endphp -
-
-
- {{ $message->body }} -
-

- {{ $message->created_at?->format('H:i') }} -

-
-
- @empty -
-
-

Henüz mesaj yok.

-

Aşağıdaki hazır metinlerden birini seçebilir veya yeni mesaj yazabilirsin.

-
-
- @endforelse -
- -
-
- @foreach($quickMessages as $quickMessage) - - @csrf - - @if($selectedCategoryId) - - @endif - - - - - @endforeach -
-
- @csrf - - @if($selectedCategoryId) - - @endif - - - -
- @error('message') -

{{ $message }}

- @enderror -
- @else -
-
-

Mesajlaşma için bir sohbet seç.

-

İlan detayından veya favori ilan satırındaki "Mesaj Gönder" butonundan yeni sohbet başlatabilirsin.

-
-
- @endif -
-
-
-
@endif @if($activeTab === 'searches') diff --git a/Modules/Listing/Database/Seeders/ListingCustomFieldSeeder.php b/Modules/Listing/Database/Seeders/ListingCustomFieldSeeder.php new file mode 100644 index 000000000..6da987de9 --- /dev/null +++ b/Modules/Listing/Database/Seeders/ListingCustomFieldSeeder.php @@ -0,0 +1,28 @@ +command) { + $this->command->info("Seeded {$seededCount} listing custom fields for {$categories->count()} categories."); + } + } +} diff --git a/Modules/Listing/Models/ListingCustomField.php b/Modules/Listing/Models/ListingCustomField.php index 493c5c6c1..a02d1e4c7 100644 --- a/Modules/Listing/Models/ListingCustomField.php +++ b/Modules/Listing/Models/ListingCustomField.php @@ -4,6 +4,7 @@ namespace Modules\Listing\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Modules\Category\Models\Category; class ListingCustomField extends Model { @@ -81,4 +82,22 @@ class ListingCustomField extends Model return collect($options)->mapWithKeys(fn (string $option): array => [$option => $option])->all(); } + + public static function upsertSeeded(Category $category, array $attributes): self + { + return static::query()->updateOrCreate( + ['name' => (string) ($attributes['name'] ?? '')], + [ + 'label' => (string) ($attributes['label'] ?? ''), + 'type' => (string) ($attributes['type'] ?? self::TYPE_TEXT), + 'category_id' => (int) $category->getKey(), + 'placeholder' => $attributes['placeholder'] ?? null, + 'help_text' => $attributes['help_text'] ?? null, + 'options' => $attributes['options'] ?? null, + 'is_required' => (bool) ($attributes['is_required'] ?? false), + 'is_active' => (bool) ($attributes['is_active'] ?? true), + 'sort_order' => (int) ($attributes['sort_order'] ?? 0), + ], + ); + } } diff --git a/Modules/Listing/Support/ListingCustomFieldSeedCatalog.php b/Modules/Listing/Support/ListingCustomFieldSeedCatalog.php new file mode 100644 index 000000000..d7bd2d67e --- /dev/null +++ b/Modules/Listing/Support/ListingCustomFieldSeedCatalog.php @@ -0,0 +1,296 @@ +slug), + ); + + return collect($definitions) + ->values() + ->map(function (array $definition, int $index) use ($category): array { + return [ + 'name' => self::fieldName((string) $category->slug, (string) $definition['name']), + 'label' => (string) $definition['label'], + 'type' => (string) $definition['type'], + 'placeholder' => $definition['placeholder'] ?? null, + 'help_text' => $definition['help_text'] ?? null, + 'options' => $definition['options'] ?? null, + 'is_required' => (bool) ($definition['is_required'] ?? false), + 'is_active' => true, + 'sort_order' => ($index + 1) * 10, + ]; + }) + ->all(); + } + + private static function rootSlug(Category $category): string + { + $current = $category; + + while ($current->parent) { + $current = $current->parent; + } + + return (string) $current->slug; + } + + private static function fieldName(string $categorySlug, string $name): string + { + return Str::slug($categorySlug . '_' . $name, '_'); + } + + private static function familyDefinitions(string $rootSlug): array + { + return match ($rootSlug) { + 'electronics' => [ + self::text('brand', 'Brand', 'Apple, Samsung, Sony'), + self::text('model', 'Model', 'iPhone 14, XPS 13'), + self::select('condition', 'Condition', ['New', 'Like New', 'Used', 'For Parts']), + self::boolean('warranty_available', 'Warranty Available'), + ], + 'vehicles' => [ + self::text('brand', 'Brand', 'Toyota, Honda, Ford'), + self::text('model', 'Model', 'Corolla, Civic'), + self::number('year', 'Year', '2021'), + self::select('condition', 'Condition', ['New', 'Excellent', 'Good', 'Fair']), + ], + 'real-estate' => [ + self::select('property_type', 'Property Type', ['Apartment', 'House', 'Villa', 'Office', 'Land', 'Shop']), + self::number('area_sqm', 'Area (sqm)', '120'), + self::boolean('furnished', 'Furnished'), + self::date('available_from', 'Available From'), + ], + 'fashion' => [ + self::text('brand', 'Brand', 'Nike, Zara, H&M'), + self::select('size', 'Size', ['XS', 'S', 'M', 'L', 'XL', 'XXL']), + self::select('condition', 'Condition', ['New with Tags', 'Like New', 'Used']), + self::text('color', 'Color', 'Black'), + ], + 'home-garden' => [ + self::text('brand', 'Brand', 'IKEA, Bosch, Philips'), + self::select('condition', 'Condition', ['New', 'Like New', 'Used']), + self::text('material', 'Material', 'Wood, Steel, Fabric'), + self::boolean('delivery_available', 'Delivery Available'), + ], + 'sports' => [ + self::text('brand', 'Brand', 'Adidas, Decathlon, Wilson'), + self::select('condition', 'Condition', ['New', 'Like New', 'Used']), + self::select('age_group', 'Age Group', ['Kids', 'Teen', 'Adult']), + self::text('sport_level', 'Skill Level', 'Beginner, Intermediate'), + ], + 'jobs' => [ + self::text('company_name', 'Company Name', 'OpenClassify'), + self::select('experience_level', 'Experience Level', ['Entry', 'Mid', 'Senior', 'Lead']), + self::boolean('remote', 'Remote'), + self::date('start_date', 'Start Date'), + ], + 'services' => [ + self::text('provider_name', 'Provider Name', 'Company or individual name'), + self::text('response_time', 'Response Time', 'Within 2 hours'), + self::boolean('on_site', 'On Site Service'), + self::select('pricing_model', 'Pricing Model', ['Fixed', 'Hourly', 'Per Project']), + ], + default => [], + }; + } + + private static function categoryDefinitions(string $categorySlug): array + { + return match ($categorySlug) { + 'electronics-phones' => [ + self::number('storage_gb', 'Storage (GB)', '128'), + self::number('battery_health', 'Battery Health (%)', '92'), + self::text('color', 'Color', 'Midnight Black'), + ], + 'electronics-computers' => [ + self::text('processor', 'Processor', 'Apple M2, Intel i7'), + self::number('ram_gb', 'RAM (GB)', '16'), + self::number('storage_gb', 'Storage (GB)', '512'), + ], + 'electronics-tablets' => [ + self::number('screen_size_inch', 'Screen Size (inch)', '11'), + self::number('storage_gb', 'Storage (GB)', '256'), + self::select('connectivity', 'Connectivity', ['Wi-Fi', 'Wi-Fi + Cellular']), + ], + 'electronics-tvs' => [ + self::number('screen_size_inch', 'Screen Size (inch)', '55'), + self::select('resolution', 'Resolution', ['HD', 'Full HD', '4K', '8K']), + self::boolean('smart_tv', 'Smart TV'), + ], + 'vehicles-cars' => [ + self::number('mileage_km', 'Mileage (km)', '85000'), + self::select('transmission', 'Transmission', ['Manual', 'Automatic', 'Semi Automatic']), + self::select('fuel_type', 'Fuel Type', ['Gasoline', 'Diesel', 'Hybrid', 'Electric', 'LPG']), + ], + 'vehicles-motorcycles' => [ + self::number('engine_cc', 'Engine (cc)', '650'), + self::number('mileage_km', 'Mileage (km)', '24000'), + self::select('fuel_type', 'Fuel Type', ['Gasoline', 'Electric']), + ], + 'vehicles-trucks' => [ + self::number('mileage_km', 'Mileage (km)', '120000'), + self::number('payload_kg', 'Payload (kg)', '3500'), + self::number('axle_count', 'Axle Count', '2'), + ], + 'vehicles-boats' => [ + self::number('length_ft', 'Length (ft)', '24'), + self::number('engine_hours', 'Engine Hours', '430'), + self::text('hull_material', 'Hull Material', 'Fiberglass'), + ], + 'real-estate-for-sale' => [ + self::number('bedrooms', 'Bedrooms', '3'), + self::number('bathrooms', 'Bathrooms', '2'), + self::boolean('title_deed_ready', 'Title Deed Ready'), + ], + 'real-estate-for-rent' => [ + self::number('bedrooms', 'Bedrooms', '2'), + self::number('bathrooms', 'Bathrooms', '1'), + self::number('deposit_amount', 'Deposit Amount', '25000'), + ], + 'real-estate-commercial' => [ + self::select('property_use', 'Property Use', ['Office', 'Shop', 'Warehouse', 'Workshop']), + self::number('parking_spaces', 'Parking Spaces', '3'), + self::text('heating', 'Heating', 'Central, VRF'), + ], + 'fashion-men', 'fashion-women', 'fashion-kids' => [ + self::text('material', 'Material', 'Cotton'), + self::select('season', 'Season', ['Spring', 'Summer', 'Autumn', 'Winter']), + self::boolean('original_packaging', 'Original Packaging'), + ], + 'fashion-shoes' => [ + self::number('eu_size', 'EU Size', '42'), + self::text('material', 'Material', 'Leather'), + self::boolean('box_included', 'Box Included'), + ], + 'home-garden-furniture' => [ + self::number('width_cm', 'Width (cm)', '180'), + self::number('height_cm', 'Height (cm)', '85'), + self::boolean('assembly_required', 'Assembly Required'), + ], + 'home-garden-garden' => [ + self::select('power_source', 'Power Source', ['Manual', 'Electric', 'Battery', 'Fuel']), + self::number('usage_area_sqm', 'Usage Area (sqm)', '450'), + self::boolean('included_tools', 'Included Tools'), + ], + 'home-garden-appliances' => [ + self::select('energy_rating', 'Energy Rating', ['A', 'A+', 'A++', 'A+++', 'B', 'C']), + self::boolean('installation_available', 'Installation Available'), + self::boolean('warranty_available', 'Warranty Available'), + ], + 'sports-outdoor' => [ + self::select('activity_type', 'Activity Type', ['Camping', 'Hiking', 'Cycling', 'Fishing']), + self::number('weight_kg', 'Weight (kg)', '12'), + self::boolean('waterproof', 'Waterproof'), + ], + 'sports-fitness' => [ + self::text('equipment_type', 'Equipment Type', 'Treadmill, Bench'), + self::number('max_weight_kg', 'Max Weight (kg)', '150'), + self::boolean('foldable', 'Foldable'), + ], + 'sports-team-sports' => [ + self::select('sport_type', 'Sport Type', ['Football', 'Basketball', 'Volleyball', 'Handball']), + self::select('official_size', 'Official Size', ['Yes', 'No']), + self::boolean('team_set_included', 'Team Set Included'), + ], + 'jobs-full-time' => [ + self::number('salary_monthly', 'Monthly Salary', '60000'), + self::select('contract_type', 'Contract Type', ['Permanent', 'Contract']), + self::textarea('benefits', 'Benefits', 'Health insurance, meal card, bonus'), + ], + 'jobs-part-time' => [ + self::number('hourly_rate', 'Hourly Rate', '350'), + self::number('weekly_hours', 'Weekly Hours', '24'), + self::text('schedule', 'Schedule', 'Weekday evenings'), + ], + 'jobs-freelance' => [ + self::text('project_length', 'Project Length', '3 months'), + self::number('budget', 'Project Budget', '45000'), + self::text('payment_terms', 'Payment Terms', '50% upfront, 50% on delivery'), + ], + 'services-cleaning' => [ + self::select('service_scope', 'Service Scope', ['Home', 'Office', 'Move-out', 'Deep Cleaning']), + self::boolean('eco_friendly', 'Eco Friendly Products'), + self::boolean('same_day_available', 'Same Day Available'), + ], + 'services-repair' => [ + self::text('repair_type', 'Repair Type', 'Phone, appliance, AC'), + self::boolean('emergency_service', 'Emergency Service'), + self::number('warranty_days', 'Warranty (days)', '90'), + ], + 'services-education' => [ + self::text('subject', 'Subject', 'Math, English, Coding'), + self::select('delivery_mode', 'Delivery Mode', ['Online', 'In Person', 'Hybrid']), + self::number('lesson_duration_minutes', 'Lesson Duration (minutes)', '60'), + ], + default => [], + }; + } + + private static function text(string $name, string $label, ?string $placeholder = null): array + { + return [ + 'name' => $name, + 'label' => $label, + 'type' => ListingCustomField::TYPE_TEXT, + 'placeholder' => $placeholder, + ]; + } + + private static function textarea(string $name, string $label, ?string $placeholder = null): array + { + return [ + 'name' => $name, + 'label' => $label, + 'type' => ListingCustomField::TYPE_TEXTAREA, + 'placeholder' => $placeholder, + ]; + } + + private static function number(string $name, string $label, ?string $placeholder = null): array + { + return [ + 'name' => $name, + 'label' => $label, + 'type' => ListingCustomField::TYPE_NUMBER, + 'placeholder' => $placeholder, + ]; + } + + private static function select(string $name, string $label, array $options): array + { + return [ + 'name' => $name, + 'label' => $label, + 'type' => ListingCustomField::TYPE_SELECT, + 'options' => $options, + ]; + } + + private static function boolean(string $name, string $label): array + { + return [ + 'name' => $name, + 'label' => $label, + 'type' => ListingCustomField::TYPE_BOOLEAN, + ]; + } + + private static function date(string $name, string $label): array + { + return [ + 'name' => $name, + 'label' => $label, + 'type' => ListingCustomField::TYPE_DATE, + ]; + } +} diff --git a/app/Livewire/PanelQuickListingForm.php b/app/Livewire/PanelQuickListingForm.php index d85cf4ead..bb27389ed 100644 --- a/app/Livewire/PanelQuickListingForm.php +++ b/app/Livewire/PanelQuickListingForm.php @@ -255,11 +255,23 @@ class PanelQuickListingForm extends Component { return match ($this->currentStep) { 1 => 'Photos', - 2 => 'Category Selection', - 3 => 'Listing Details', - 4 => 'Attributes', - 5 => 'Preview', - default => 'Create Listing', + 2 => 'Category', + 3 => 'Basics', + 4 => 'Details', + 5 => 'Review', + default => 'New Listing', + }; + } + + public function getCurrentStepHintProperty(): string + { + return match ($this->currentStep) { + 1 => 'Add photos first.', + 2 => 'Pick the right category.', + 3 => 'Add the basics.', + 4 => 'Add extra details if needed.', + 5 => 'Check everything before publishing.', + default => 'Create a new listing.', }; } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index f713d56d5..b6c454c9b 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -32,6 +32,7 @@ class DatabaseSeeder extends Seeder HomeSliderSettingsSeeder::class, \Modules\Location\Database\Seeders\LocationSeeder::class, \Modules\Category\Database\Seeders\CategorySeeder::class, + \Modules\Listing\Database\Seeders\ListingCustomFieldSeeder::class, \Modules\Listing\Database\Seeders\ListingSeeder::class, ]); } diff --git a/resources/css/app.css b/resources/css/app.css index 7767e0e5f..8fde9551f 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -87,14 +87,21 @@ h6 { .oc-nav-wrap { max-width: 1320px; margin: 0 auto; - padding: 18px 16px 14px; + padding: 12px 16px; } .oc-nav-main { display: grid; - grid-template-columns: auto minmax(320px, 1fr) auto; + grid-template-columns: minmax(0, 1fr); + align-items: stretch; + gap: 12px; +} + +.oc-topbar { + display: flex; align-items: center; - gap: 18px; + justify-content: space-between; + gap: 12px; } .oc-brand { @@ -104,11 +111,16 @@ h6 { min-width: 0; } +.oc-topbar .oc-brand { + flex: 1 1 auto; +} + .oc-search { display: flex; align-items: center; gap: 12px; - min-height: 56px; + width: 100%; + min-height: 52px; padding: 0 14px 0 18px; border: 1px solid rgba(29, 29, 31, 0.08); border-radius: 999px; @@ -116,6 +128,10 @@ h6 { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75); } +.oc-search-main { + grid-column: 1 / -1; +} + .oc-search-icon { color: #6e6e73; flex-shrink: 0; @@ -146,18 +162,37 @@ h6 { } .oc-actions { - display: inline-flex; + display: flex; align-items: center; - justify-content: flex-end; - gap: 12px; + gap: 10px; + flex-wrap: wrap; +} + +.oc-location { + position: relative; + flex: 1 1 0; + min-width: 0; +} + +.oc-location-trigger { + justify-content: space-between; + width: 100%; +} + +.oc-location-label { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .oc-pill { display: inline-flex; align-items: center; gap: 10px; - min-height: 48px; - padding: 0 18px; + min-height: 44px; + padding: 0 16px; border: 1px solid rgba(29, 29, 31, 0.08); border-radius: 999px; background: rgba(255, 255, 255, 0.84); @@ -177,10 +212,11 @@ h6 { display: inline-flex; align-items: center; justify-content: center; - min-height: 48px; - padding: 0 22px; + min-height: 44px; + padding: 0 18px; font-size: 0.96rem; font-weight: 600; + flex-shrink: 0; } .oc-text-link { @@ -194,20 +230,182 @@ h6 { color: var(--oc-text); } -.oc-mobile-tools { - display: grid; - gap: 12px; - margin-top: 14px; +.oc-auth-link { + display: none; + align-items: center; + min-height: 44px; } -.oc-mobile-pills { +.oc-logout { + display: none; +} + +.oc-mobile-menu-shell { + position: fixed; + inset: 0; + z-index: 80; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.22s ease; +} + +.oc-mobile-menu-shell.is-open { + opacity: 1; + visibility: visible; + pointer-events: auto; +} + +.oc-mobile-menu-backdrop { + position: absolute; + inset: 0; + border: 0; + background: rgba(15, 23, 42, 0.22); + backdrop-filter: saturate(150%) blur(16px); +} + +.oc-mobile-menu-panel { + position: absolute; + top: 12px; + left: 12px; + right: 12px; + max-height: calc(100vh - 24px); + overflow: auto; + padding: 18px; + border: 1px solid rgba(255, 255, 255, 0.75); + border-radius: 28px; + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 24px 64px rgba(15, 23, 42, 0.22); + transform: translateY(-12px) scale(0.985); + transition: transform 0.24s ease; +} + +.oc-mobile-menu-shell.is-open .oc-mobile-menu-panel { + transform: translateY(0) scale(1); +} + +.oc-mobile-menu-header { display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; +} + +.oc-mobile-menu-title { + font-size: 1.45rem; + font-weight: 700; + letter-spacing: -0.03em; + line-height: 1; +} + +.oc-mobile-menu-close { + display: inline-flex; + flex-shrink: 0; +} + +.oc-mobile-menu-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; - overflow-x: auto; - padding-bottom: 2px; + margin-top: 16px; +} + +.oc-mobile-menu-primary { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 48px; + padding: 0 16px; + border-radius: 16px; + border: 1px solid rgba(29, 29, 31, 0.08); + background: rgba(255, 255, 255, 0.92); + color: var(--oc-text); + font-size: 0.95rem; + font-weight: 700; +} + +.oc-mobile-menu-primary-strong { + background: linear-gradient(180deg, #2997ff, var(--oc-primary)); + color: #fff; + border-color: transparent; +} + +.oc-mobile-menu-section { + margin-top: 16px; +} + +.oc-mobile-menu-label { + margin-bottom: 8px; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #6b7280; +} + +.oc-mobile-menu-list { + display: grid; + gap: 8px; +} + +.oc-mobile-menu-link { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 54px; + padding: 0 18px; + border: 1px solid rgba(29, 29, 31, 0.08); + border-radius: 18px; + background: rgba(255, 255, 255, 0.92); + font-size: 0.98rem; + font-weight: 600; + color: var(--oc-text); +} + +.oc-mobile-menu-languages { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.oc-mobile-menu-language { + display: inline-flex; + align-items: center; + min-height: 38px; + padding: 0 12px; + border-radius: 999px; + border: 1px solid rgba(29, 29, 31, 0.08); + background: rgba(255, 255, 255, 0.92); + color: #475569; + font-size: 0.82rem; + font-weight: 600; +} + +.oc-mobile-menu-language.is-active { + color: var(--oc-primary); + border-color: var(--oc-primary-soft-border); + background: var(--oc-primary-soft); +} + +.oc-mobile-menu-logout { + margin-top: 18px; +} + +.oc-mobile-menu-logout-btn { + width: 100%; + min-height: 52px; + border: 0; + border-radius: 18px; + background: #111827; + color: #fff; + font-size: 0.96rem; + font-weight: 700; + cursor: pointer; } .oc-category-row { + display: none; margin-top: 14px; padding-top: 14px; border-top: 1px solid rgba(29, 29, 31, 0.08); @@ -284,23 +482,35 @@ h6 { .header-utility { width: 2.75rem; height: 2.75rem; + flex: 0 0 2.75rem; border-radius: 999px; border: 1px solid var(--oc-border); background: #ffffff; - display: inline-flex; + display: none; align-items: center; justify-content: center; color: #4b5563; transition: all 0.2s ease; } +.header-utility svg { + width: 1.125rem; + height: 1.125rem; +} + +.header-utility.oc-compact-menu-trigger, +.header-utility.oc-mobile-menu-close { + display: inline-flex; + flex-shrink: 0; +} + .header-utility:hover { border-color: var(--oc-primary-soft-border); color: var(--oc-primary); } .location-panel { - width: min(90vw, 360px); + width: min(calc(100vw - 32px), 360px); } .location-panel select { @@ -312,55 +522,120 @@ 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) { +@media (min-width: 768px) { .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; + flex-wrap: nowrap; + } + + .oc-location { + flex: 0 1 240px; + } + + .oc-mobile-menu-panel { + top: 16px; + right: 16px; + left: auto; + width: min(420px, calc(100vw - 32px)); + max-height: calc(100vh - 32px); } } -@media (max-width: 767px) { - .oc-nav-main { - grid-template-columns: 1fr; +@media (min-width: 1024px) { + .oc-nav-wrap { + padding: 18px 16px 14px; } - .oc-brand { - justify-content: center; + .oc-nav-main { + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 16px; + } + + .oc-topbar { + min-width: 0; + gap: 16px; + } + + .oc-search-main { + grid-column: auto; + } + + .oc-search { + min-height: 3.35rem; + padding: 0 16px 0 18px; + } + + .oc-search-submit { + min-width: 5.25rem; + text-align: center; } .oc-actions { + justify-content: flex-end; + gap: 12px; + flex-wrap: nowrap; + } + + .oc-location { + flex: 0 0 192px; + } + + .oc-pill, + .oc-cta { + min-height: 3rem; + } + + .oc-cta { + padding: 0 22px; + } + + .header-utility { + width: 3rem; + height: 3rem; + flex-basis: 3rem; + } + + .oc-text-link { + min-height: 3rem; + display: inline-flex; + align-items: center; + } + + .oc-auth-link { + display: inline-flex; + } + + .oc-desktop-utility { + display: inline-flex; + } + + .oc-logout { + display: block; + } + + .header-utility.oc-compact-menu-trigger, + .oc-mobile-menu-shell { display: none; } .oc-category-row { - margin-top: 12px; - padding-top: 12px; + display: block; + } +} + +@media (min-width: 1280px) { + .brand-text { + font-size: 1.7rem; } } diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index d81d5664c..97fee6946 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -28,6 +28,11 @@ 'ja' => '日本語', ]; $headerCategories = collect($headerNavCategories ?? [])->values(); + $menuBrowseLinks = collect([ + ['label' => 'Home', 'url' => route('home')], + ['label' => 'All Listings', 'url' => route('listings.index')], + ['label' => 'Categories', 'url' => route('categories.index')], + ]); $locationCountries = collect($headerLocationCountries ?? [])->values(); $defaultCountryIso2 = strtoupper((string) config('app.default_country_iso2', 'TR')); $citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities') @@ -48,16 +53,31 @@