mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
Refactor modules UI and seeders
This commit is contained in:
parent
63f2c95fd7
commit
6ee6da3d83
@ -57,7 +57,6 @@ final class HomeSlideFormSchema
|
|||||||
->reorderableWithButtons()
|
->reorderableWithButtons()
|
||||||
->addActionLabel('Add Slide')
|
->addActionLabel('Add Slide')
|
||||||
->itemLabel(fn (array $state): string => filled($state['title'] ?? null) ? (string) $state['title'] : 'New 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));
|
->dehydrateStateUsing(fn ($state) => $normalizeSlides($state));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,6 +95,16 @@ class Category extends Model
|
|||||||
->get(['id', 'name', 'slug']);
|
->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
|
public static function rootTreeWithActiveChildren(): Collection
|
||||||
{
|
{
|
||||||
return static::query()
|
return static::query()
|
||||||
|
|||||||
@ -8,7 +8,6 @@ use Illuminate\Pagination\LengthAwarePaginator;
|
|||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
use Modules\Conversation\App\Models\Conversation;
|
use Modules\Conversation\App\Models\Conversation;
|
||||||
use Modules\Conversation\App\Support\QuickMessageCatalog;
|
|
||||||
use Modules\Favorite\App\Models\FavoriteSearch;
|
use Modules\Favorite\App\Models\FavoriteSearch;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
use Modules\User\App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
@ -28,11 +27,6 @@ class FavoriteController extends Controller
|
|||||||
$statusFilter = 'all';
|
$statusFilter = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
$messageFilter = (string) $request->string('message_filter', 'all');
|
|
||||||
if (! in_array($messageFilter, ['all', 'unread', 'important'], true)) {
|
|
||||||
$messageFilter = 'all';
|
|
||||||
}
|
|
||||||
|
|
||||||
$selectedCategoryId = $request->integer('category');
|
$selectedCategoryId = $request->integer('category');
|
||||||
if ($selectedCategoryId <= 0) {
|
if ($selectedCategoryId <= 0) {
|
||||||
$selectedCategoryId = null;
|
$selectedCategoryId = null;
|
||||||
@ -52,8 +46,6 @@ class FavoriteController extends Controller
|
|||||||
$favoriteListings = $this->emptyPaginator();
|
$favoriteListings = $this->emptyPaginator();
|
||||||
$favoriteSearches = $this->emptyPaginator();
|
$favoriteSearches = $this->emptyPaginator();
|
||||||
$favoriteSellers = $this->emptyPaginator();
|
$favoriteSellers = $this->emptyPaginator();
|
||||||
$conversations = collect();
|
|
||||||
$selectedConversation = null;
|
|
||||||
$buyerConversationListingMap = [];
|
$buyerConversationListingMap = [];
|
||||||
|
|
||||||
if ($user && $activeTab === 'listings') {
|
if ($user && $activeTab === 'listings') {
|
||||||
@ -69,34 +61,20 @@ class FavoriteController extends Controller
|
|||||||
->withQueryString();
|
->withQueryString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->tableExists('conversations') && $this->tableExists('conversation_messages')) {
|
if (
|
||||||
|
$favoriteListings->isNotEmpty()
|
||||||
|
&& $this->tableExists('conversations')
|
||||||
|
) {
|
||||||
$userId = (int) $user->getKey();
|
$userId = (int) $user->getKey();
|
||||||
$conversations = Conversation::inboxForUser($userId, $messageFilter);
|
$buyerConversationListingMap = Conversation::query()
|
||||||
$buyerConversationListingMap = $conversations
|
|
||||||
->where('buyer_id', $userId)
|
->where('buyer_id', $userId)
|
||||||
|
->whereIn('listing_id', $favoriteListings->pluck('id')->all())
|
||||||
->pluck('id', 'listing_id')
|
->pluck('id', 'listing_id')
|
||||||
->map(fn ($conversationId) => (int) $conversationId)
|
->map(fn ($conversationId) => (int) $conversationId)
|
||||||
->all();
|
->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) {
|
} catch (Throwable) {
|
||||||
$favoriteListings = $this->emptyPaginator();
|
$favoriteListings = $this->emptyPaginator();
|
||||||
$conversations = collect();
|
|
||||||
$selectedConversation = null;
|
|
||||||
$buyerConversationListingMap = [];
|
$buyerConversationListingMap = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,15 +113,11 @@ class FavoriteController extends Controller
|
|||||||
'activeTab' => $activeTab,
|
'activeTab' => $activeTab,
|
||||||
'statusFilter' => $statusFilter,
|
'statusFilter' => $statusFilter,
|
||||||
'selectedCategoryId' => $selectedCategoryId,
|
'selectedCategoryId' => $selectedCategoryId,
|
||||||
'messageFilter' => $messageFilter,
|
|
||||||
'categories' => $categories,
|
'categories' => $categories,
|
||||||
'favoriteListings' => $favoriteListings,
|
'favoriteListings' => $favoriteListings,
|
||||||
'favoriteSearches' => $favoriteSearches,
|
'favoriteSearches' => $favoriteSearches,
|
||||||
'favoriteSellers' => $favoriteSellers,
|
'favoriteSellers' => $favoriteSellers,
|
||||||
'conversations' => $conversations,
|
|
||||||
'selectedConversation' => $selectedConversation,
|
|
||||||
'buyerConversationListingMap' => $buyerConversationListingMap,
|
'buyerConversationListingMap' => $buyerConversationListingMap,
|
||||||
'quickMessages' => QuickMessageCatalog::all(),
|
|
||||||
'requiresLogin' => $requiresLogin,
|
'requiresLogin' => $requiresLogin,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,6 @@
|
|||||||
'tab' => 'listings',
|
'tab' => 'listings',
|
||||||
'status' => $statusFilter,
|
'status' => $statusFilter,
|
||||||
'category' => $selectedCategoryId,
|
'category' => $selectedCategoryId,
|
||||||
'message_filter' => $messageFilter,
|
|
||||||
], fn ($value) => !is_null($value) && $value !== '');
|
], fn ($value) => !is_null($value) && $value !== '');
|
||||||
@endphp
|
@endphp
|
||||||
<div class="border-b-2 border-blue-900 px-4 py-3 flex flex-wrap items-center gap-3">
|
<div class="border-b-2 border-blue-900 px-4 py-3 flex flex-wrap items-center gap-3">
|
||||||
@ -42,7 +41,6 @@
|
|||||||
<form method="GET" action="{{ route('favorites.index') }}" class="flex items-center gap-2">
|
<form method="GET" action="{{ route('favorites.index') }}" class="flex items-center gap-2">
|
||||||
<input type="hidden" name="tab" value="listings">
|
<input type="hidden" name="tab" value="listings">
|
||||||
<input type="hidden" name="status" value="{{ $statusFilter }}">
|
<input type="hidden" name="status" value="{{ $statusFilter }}">
|
||||||
<input type="hidden" name="message_filter" value="{{ $messageFilter }}">
|
|
||||||
<select name="category" class="h-10 min-w-44 border border-slate-300 px-3 text-sm text-slate-700">
|
<select name="category" class="h-10 min-w-44 border border-slate-300 px-3 text-sm text-slate-700">
|
||||||
<option value="">Kategori</option>
|
<option value="">Kategori</option>
|
||||||
@foreach($categories as $category)
|
@foreach($categories as $category)
|
||||||
@ -106,11 +104,6 @@
|
|||||||
@else
|
@else
|
||||||
<form method="POST" action="{{ route('conversations.start', $listing) }}">
|
<form method="POST" action="{{ route('conversations.start', $listing) }}">
|
||||||
@csrf
|
@csrf
|
||||||
<input type="hidden" name="status" value="{{ $statusFilter }}">
|
|
||||||
@if($selectedCategoryId)
|
|
||||||
<input type="hidden" name="category" value="{{ $selectedCategoryId }}">
|
|
||||||
@endif
|
|
||||||
<input type="hidden" name="message_filter" value="{{ $messageFilter }}">
|
|
||||||
<button type="submit" class="inline-flex items-center h-10 px-4 bg-rose-500 text-white text-sm font-semibold rounded-full hover:bg-rose-600 transition">
|
<button type="submit" class="inline-flex items-center h-10 px-4 bg-rose-500 text-white text-sm font-semibold rounded-full hover:bg-rose-600 transition">
|
||||||
Mesaj Gönder
|
Mesaj Gönder
|
||||||
</button>
|
</button>
|
||||||
@ -145,176 +138,6 @@
|
|||||||
@if($favoriteListings?->hasPages())
|
@if($favoriteListings?->hasPages())
|
||||||
<div class="px-4 pb-4">{{ $favoriteListings->links() }}</div>
|
<div class="px-4 pb-4">{{ $favoriteListings->links() }}</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="border-t border-slate-200 bg-slate-50 p-4 sm:p-5">
|
|
||||||
<div class="border border-slate-200 bg-white rounded-2xl overflow-hidden shadow-sm">
|
|
||||||
<div class="grid grid-cols-1 xl:grid-cols-[420px,1fr] min-h-[620px]">
|
|
||||||
<div class="border-b xl:border-b-0 xl:border-r border-slate-200">
|
|
||||||
<div class="px-6 py-5 border-b border-slate-200 flex items-center justify-between gap-3">
|
|
||||||
<h2 class="text-3xl font-bold text-slate-900">Gelen Kutusu</h2>
|
|
||||||
<svg class="w-6 h-6 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35m1.6-5.05a7.25 7.25 0 11-14.5 0 7.25 7.25 0 0114.5 0z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="px-6 py-4 border-b border-slate-200">
|
|
||||||
<p class="text-sm font-semibold text-slate-600 mb-2">Hızlı Filtreler</p>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<a href="{{ route('favorites.index', array_merge($listingTabQuery, ['message_filter' => 'all'])) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold border {{ $messageFilter === 'all' ? 'border-rose-400 bg-rose-50 text-rose-600' : 'border-slate-300 text-slate-600 hover:bg-slate-100' }}">
|
|
||||||
Hepsi
|
|
||||||
</a>
|
|
||||||
<a href="{{ route('favorites.index', array_merge($listingTabQuery, ['message_filter' => 'unread'])) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold border {{ $messageFilter === 'unread' ? 'border-rose-400 bg-rose-50 text-rose-600' : 'border-slate-300 text-slate-600 hover:bg-slate-100' }}">
|
|
||||||
Okunmamış
|
|
||||||
</a>
|
|
||||||
<a href="{{ route('favorites.index', array_merge($listingTabQuery, ['message_filter' => 'important'])) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold border {{ $messageFilter === 'important' ? 'border-rose-400 bg-rose-50 text-rose-600' : 'border-slate-300 text-slate-600 hover:bg-slate-100' }}">
|
|
||||||
Önemli
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="max-h-[480px] overflow-y-auto divide-y divide-slate-200">
|
|
||||||
@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
|
|
||||||
<a href="{{ route('favorites.index', array_merge($listingTabQuery, ['conversation' => $conversation->id])) }}" class="block px-6 py-4 transition {{ $isSelected ? 'bg-rose-50' : 'hover:bg-slate-50' }}">
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<div class="w-14 h-14 rounded-xl bg-slate-100 border border-slate-200 overflow-hidden shrink-0">
|
|
||||||
@if($conversationImage)
|
|
||||||
<img src="{{ $conversationImage }}" alt="{{ $conversationListing?->title }}" class="w-full h-full object-cover">
|
|
||||||
@else
|
|
||||||
<div class="w-full h-full grid place-items-center text-slate-400 text-xs">İlan</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<div class="flex items-start gap-2">
|
|
||||||
<p class="font-semibold text-2xl text-slate-900 truncate">{{ $partner?->name ?? 'Kullanıcı' }}</p>
|
|
||||||
<p class="text-xs text-slate-500 whitespace-nowrap ml-auto">{{ $conversation->last_message_at?->format('d.m.Y') }}</p>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-slate-500 truncate mt-1">{{ $conversationListing?->title ?? 'İlan silinmiş' }}</p>
|
|
||||||
<p class="text-sm {{ $conversation->unread_count > 0 ? 'text-slate-900 font-semibold' : 'text-slate-500' }} truncate mt-1">
|
|
||||||
{{ $lastMessage !== '' ? $lastMessage : 'Henüz mesaj yok' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
@if($conversation->unread_count > 0)
|
|
||||||
<span class="inline-flex items-center justify-center min-w-6 h-6 px-2 rounded-full bg-rose-500 text-white text-xs font-semibold">
|
|
||||||
{{ $conversation->unread_count }}
|
|
||||||
</span>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
@empty
|
|
||||||
<div class="px-6 py-16 text-center text-slate-500">
|
|
||||||
Henüz bir sohbetin yok.
|
|
||||||
</div>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col min-h-[620px]">
|
|
||||||
@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
|
|
||||||
<div class="h-24 px-6 border-b border-slate-200 flex items-center gap-4">
|
|
||||||
<div class="w-12 h-12 rounded-full bg-slate-600 text-white grid place-items-center font-semibold text-lg">
|
|
||||||
{{ strtoupper(substr((string) ($activePartner?->name ?? 'K'), 0, 1)) }}
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0">
|
|
||||||
<p class="text-3xl font-bold text-slate-900 truncate">{{ $activePartner?->name ?? 'Kullanıcı' }}</p>
|
|
||||||
<p class="text-sm text-slate-500 truncate">{{ $activeListing?->title ?? 'İlan silinmiş' }}</p>
|
|
||||||
</div>
|
|
||||||
@if($activePriceLabel)
|
|
||||||
<div class="ml-auto text-3xl font-semibold text-slate-800 whitespace-nowrap">{{ $activePriceLabel }}</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 px-6 py-6 bg-slate-100/60 overflow-y-auto max-h-[390px]">
|
|
||||||
@forelse($selectedConversation->messages as $message)
|
|
||||||
@php $isMine = (int) $message->sender_id === (int) auth()->id(); @endphp
|
|
||||||
<div class="mb-4 flex {{ $isMine ? 'justify-end' : 'justify-start' }}">
|
|
||||||
<div class="max-w-[80%]">
|
|
||||||
<div class="{{ $isMine ? 'bg-amber-100 text-slate-900' : 'bg-white text-slate-900 border border-slate-200' }} rounded-2xl px-4 py-2 text-base shadow-sm">
|
|
||||||
{{ $message->body }}
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-slate-500 mt-1 {{ $isMine ? 'text-right' : 'text-left' }}">
|
|
||||||
{{ $message->created_at?->format('H:i') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@empty
|
|
||||||
<div class="h-full grid place-items-center text-slate-500 text-center px-8">
|
|
||||||
<div>
|
|
||||||
<p class="font-semibold text-slate-700">Henüz mesaj yok.</p>
|
|
||||||
<p class="text-sm mt-1">Aşağıdaki hazır metinlerden birini seçebilir veya yeni mesaj yazabilirsin.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endforelse
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="px-4 py-3 border-t border-slate-200 bg-white">
|
|
||||||
<div class="flex items-center gap-2 overflow-x-auto pb-2">
|
|
||||||
@foreach($quickMessages as $quickMessage)
|
|
||||||
<form method="POST" action="{{ route('conversations.messages.send', $selectedConversation) }}" class="shrink-0">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="status" value="{{ $statusFilter }}">
|
|
||||||
@if($selectedCategoryId)
|
|
||||||
<input type="hidden" name="category" value="{{ $selectedCategoryId }}">
|
|
||||||
@endif
|
|
||||||
<input type="hidden" name="message_filter" value="{{ $messageFilter }}">
|
|
||||||
<input type="hidden" name="message" value="{{ $quickMessage }}">
|
|
||||||
<button type="submit" class="inline-flex items-center h-11 px-5 rounded-full border border-rose-300 text-rose-600 font-semibold text-sm hover:bg-rose-50 transition">
|
|
||||||
{{ $quickMessage }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
<form method="POST" action="{{ route('conversations.messages.send', $selectedConversation) }}" class="flex items-center gap-2 border-t border-slate-200 pt-3 mt-1">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="status" value="{{ $statusFilter }}">
|
|
||||||
@if($selectedCategoryId)
|
|
||||||
<input type="hidden" name="category" value="{{ $selectedCategoryId }}">
|
|
||||||
@endif
|
|
||||||
<input type="hidden" name="message_filter" value="{{ $messageFilter }}">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="message"
|
|
||||||
value="{{ old('message') }}"
|
|
||||||
placeholder="Bir mesaj yaz"
|
|
||||||
maxlength="2000"
|
|
||||||
class="h-12 flex-1 rounded-full border border-slate-300 px-5 text-sm focus:outline-none focus:ring-2 focus:ring-rose-300"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<button type="submit" class="h-12 w-12 rounded-full bg-black text-white grid place-items-center hover:bg-slate-800 transition" aria-label="Gönder">
|
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h13m0 0l-5-5m5 5l-5 5"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
@error('message')
|
|
||||||
<p class="text-xs text-rose-600 mt-2 px-2">{{ $message }}</p>
|
|
||||||
@enderror
|
|
||||||
</div>
|
|
||||||
@else
|
|
||||||
<div class="h-full min-h-[620px] grid place-items-center px-8 text-center text-slate-500">
|
|
||||||
<div>
|
|
||||||
<p class="text-2xl font-semibold text-slate-700">Mesajlaşma için bir sohbet seç.</p>
|
|
||||||
<p class="mt-2 text-sm">İlan detayından veya favori ilan satırındaki "Mesaj Gönder" butonundan yeni sohbet başlatabilirsin.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if($activeTab === 'searches')
|
@if($activeTab === 'searches')
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Listing\Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Modules\Category\Models\Category;
|
||||||
|
use Modules\Listing\Models\ListingCustomField;
|
||||||
|
use Modules\Listing\Support\ListingCustomFieldSeedCatalog;
|
||||||
|
|
||||||
|
class ListingCustomFieldSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categories = Category::seedableListingFieldCategories();
|
||||||
|
$seededCount = 0;
|
||||||
|
|
||||||
|
foreach ($categories as $category) {
|
||||||
|
foreach (ListingCustomFieldSeedCatalog::payloadsFor($category) as $payload) {
|
||||||
|
ListingCustomField::upsertSeeded($category, $payload);
|
||||||
|
$seededCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->command) {
|
||||||
|
$this->command->info("Seeded {$seededCount} listing custom fields for {$categories->count()} categories.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ namespace Modules\Listing\Models;
|
|||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Modules\Category\Models\Category;
|
||||||
|
|
||||||
class ListingCustomField extends Model
|
class ListingCustomField extends Model
|
||||||
{
|
{
|
||||||
@ -81,4 +82,22 @@ class ListingCustomField extends Model
|
|||||||
|
|
||||||
return collect($options)->mapWithKeys(fn (string $option): array => [$option => $option])->all();
|
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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
296
Modules/Listing/Support/ListingCustomFieldSeedCatalog.php
Normal file
296
Modules/Listing/Support/ListingCustomFieldSeedCatalog.php
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Listing\Support;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Modules\Category\Models\Category;
|
||||||
|
use Modules\Listing\Models\ListingCustomField;
|
||||||
|
|
||||||
|
final class ListingCustomFieldSeedCatalog
|
||||||
|
{
|
||||||
|
public static function payloadsFor(Category $category): array
|
||||||
|
{
|
||||||
|
$definitions = array_merge(
|
||||||
|
self::familyDefinitions(self::rootSlug($category)),
|
||||||
|
self::categoryDefinitions((string) $category->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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -255,11 +255,23 @@ class PanelQuickListingForm extends Component
|
|||||||
{
|
{
|
||||||
return match ($this->currentStep) {
|
return match ($this->currentStep) {
|
||||||
1 => 'Photos',
|
1 => 'Photos',
|
||||||
2 => 'Category Selection',
|
2 => 'Category',
|
||||||
3 => 'Listing Details',
|
3 => 'Basics',
|
||||||
4 => 'Attributes',
|
4 => 'Details',
|
||||||
5 => 'Preview',
|
5 => 'Review',
|
||||||
default => 'Create Listing',
|
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.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
HomeSliderSettingsSeeder::class,
|
HomeSliderSettingsSeeder::class,
|
||||||
\Modules\Location\Database\Seeders\LocationSeeder::class,
|
\Modules\Location\Database\Seeders\LocationSeeder::class,
|
||||||
\Modules\Category\Database\Seeders\CategorySeeder::class,
|
\Modules\Category\Database\Seeders\CategorySeeder::class,
|
||||||
|
\Modules\Listing\Database\Seeders\ListingCustomFieldSeeder::class,
|
||||||
\Modules\Listing\Database\Seeders\ListingSeeder::class,
|
\Modules\Listing\Database\Seeders\ListingSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,14 +87,21 @@ h6 {
|
|||||||
.oc-nav-wrap {
|
.oc-nav-wrap {
|
||||||
max-width: 1320px;
|
max-width: 1320px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 18px 16px 14px;
|
padding: 12px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-nav-main {
|
.oc-nav-main {
|
||||||
display: grid;
|
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;
|
align-items: center;
|
||||||
gap: 18px;
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-brand {
|
.oc-brand {
|
||||||
@ -104,11 +111,16 @@ h6 {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oc-topbar .oc-brand {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.oc-search {
|
.oc-search {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 56px;
|
width: 100%;
|
||||||
|
min-height: 52px;
|
||||||
padding: 0 14px 0 18px;
|
padding: 0 14px 0 18px;
|
||||||
border: 1px solid rgba(29, 29, 31, 0.08);
|
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@ -116,6 +128,10 @@ h6 {
|
|||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oc-search-main {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.oc-search-icon {
|
.oc-search-icon {
|
||||||
color: #6e6e73;
|
color: #6e6e73;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@ -146,18 +162,37 @@ h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.oc-actions {
|
.oc-actions {
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
gap: 10px;
|
||||||
gap: 12px;
|
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 {
|
.oc-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
min-height: 48px;
|
min-height: 44px;
|
||||||
padding: 0 18px;
|
padding: 0 16px;
|
||||||
border: 1px solid rgba(29, 29, 31, 0.08);
|
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.84);
|
background: rgba(255, 255, 255, 0.84);
|
||||||
@ -177,10 +212,11 @@ h6 {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 48px;
|
min-height: 44px;
|
||||||
padding: 0 22px;
|
padding: 0 18px;
|
||||||
font-size: 0.96rem;
|
font-size: 0.96rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-text-link {
|
.oc-text-link {
|
||||||
@ -194,20 +230,182 @@ h6 {
|
|||||||
color: var(--oc-text);
|
color: var(--oc-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-mobile-tools {
|
.oc-auth-link {
|
||||||
display: grid;
|
display: none;
|
||||||
gap: 12px;
|
align-items: center;
|
||||||
margin-top: 14px;
|
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;
|
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;
|
gap: 10px;
|
||||||
overflow-x: auto;
|
margin-top: 16px;
|
||||||
padding-bottom: 2px;
|
}
|
||||||
|
|
||||||
|
.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 {
|
.oc-category-row {
|
||||||
|
display: none;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
border-top: 1px solid rgba(29, 29, 31, 0.08);
|
border-top: 1px solid rgba(29, 29, 31, 0.08);
|
||||||
@ -284,23 +482,35 @@ h6 {
|
|||||||
.header-utility {
|
.header-utility {
|
||||||
width: 2.75rem;
|
width: 2.75rem;
|
||||||
height: 2.75rem;
|
height: 2.75rem;
|
||||||
|
flex: 0 0 2.75rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid var(--oc-border);
|
border: 1px solid var(--oc-border);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
display: inline-flex;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
transition: all 0.2s ease;
|
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 {
|
.header-utility:hover {
|
||||||
border-color: var(--oc-primary-soft-border);
|
border-color: var(--oc-primary-soft-border);
|
||||||
color: var(--oc-primary);
|
color: var(--oc-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-panel {
|
.location-panel {
|
||||||
width: min(90vw, 360px);
|
width: min(calc(100vw - 32px), 360px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-panel select {
|
.location-panel select {
|
||||||
@ -312,55 +522,120 @@ h6 {
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1279px) {
|
@media (min-width: 768px) {
|
||||||
.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 {
|
.oc-nav-wrap {
|
||||||
padding-top: 14px;
|
padding-top: 14px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-nav-main {
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-text {
|
.brand-text {
|
||||||
font-size: 1.42rem;
|
font-size: 1.42rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-actions {
|
.oc-actions {
|
||||||
grid-column: auto;
|
|
||||||
justify-content: flex-end;
|
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) {
|
@media (min-width: 1024px) {
|
||||||
.oc-nav-main {
|
.oc-nav-wrap {
|
||||||
grid-template-columns: 1fr;
|
padding: 18px 16px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-brand {
|
.oc-nav-main {
|
||||||
justify-content: center;
|
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 {
|
.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;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-category-row {
|
.oc-category-row {
|
||||||
margin-top: 12px;
|
display: block;
|
||||||
padding-top: 12px;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.brand-text {
|
||||||
|
font-size: 1.7rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,11 @@
|
|||||||
'ja' => '日本語',
|
'ja' => '日本語',
|
||||||
];
|
];
|
||||||
$headerCategories = collect($headerNavCategories ?? [])->values();
|
$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();
|
$locationCountries = collect($headerLocationCountries ?? [])->values();
|
||||||
$defaultCountryIso2 = strtoupper((string) config('app.default_country_iso2', 'TR'));
|
$defaultCountryIso2 = strtoupper((string) config('app.default_country_iso2', 'TR'));
|
||||||
$citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities')
|
$citiesRouteTemplate = \Illuminate\Support\Facades\Route::has('locations.cities')
|
||||||
@ -48,16 +53,31 @@
|
|||||||
<nav class="market-nav-surface sticky top-0 z-50">
|
<nav class="market-nav-surface sticky top-0 z-50">
|
||||||
<div class="oc-nav-wrap">
|
<div class="oc-nav-wrap">
|
||||||
<div class="oc-nav-main">
|
<div class="oc-nav-main">
|
||||||
<a href="{{ route('home') }}" class="oc-brand">
|
<div class="oc-topbar">
|
||||||
@if($siteLogoUrl)
|
<a href="{{ route('home') }}" class="oc-brand">
|
||||||
<img src="{{ $siteLogoUrl }}" alt="{{ $siteName }}" class="h-9 w-auto rounded-xl">
|
@if($siteLogoUrl)
|
||||||
@else
|
<img src="{{ $siteLogoUrl }}" alt="{{ $siteName }}" class="h-9 w-auto rounded-xl">
|
||||||
<span class="brand-logo" aria-hidden="true"></span>
|
@else
|
||||||
@endif
|
<span class="brand-logo" aria-hidden="true"></span>
|
||||||
<span class="brand-text leading-none">{{ $siteName }}</span>
|
@endif
|
||||||
</a>
|
<span class="brand-text leading-none">{{ $siteName }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
<form action="{{ route('listings.index') }}" method="GET" class="oc-search hidden lg:flex">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="header-utility oc-compact-menu-trigger"
|
||||||
|
data-mobile-menu-open
|
||||||
|
aria-label="Open navigation menu"
|
||||||
|
aria-controls="oc-mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 7h16M7 12h10M10 17h4"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="{{ route('listings.index') }}" method="GET" class="oc-search oc-search-main">
|
||||||
<svg class="w-5 h-5 oc-search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5 oc-search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M21 21l-4.35-4.35m1.6-5.05a7.25 7.25 0 11-14.5 0 7.25 7.25 0 0114.5 0z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M21 21l-4.35-4.35m1.6-5.05a7.25 7.25 0 11-14.5 0 7.25 7.25 0 0114.5 0z"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -74,13 +94,13 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="oc-actions">
|
<div class="oc-actions">
|
||||||
<details class="relative hidden md:block" data-location-widget data-cities-url-template="{{ $citiesRouteTemplate }}">
|
<details class="relative oc-location" data-location-widget data-cities-url-template="{{ $citiesRouteTemplate }}">
|
||||||
<summary class="oc-pill list-none cursor-pointer">
|
<summary class="oc-pill oc-location-trigger list-none cursor-pointer">
|
||||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21s7-6.2 7-11a7 7 0 10-14 0c0 4.8 7 11 7 11z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21s7-6.2 7-11a7 7 0 10-14 0c0 4.8 7 11 7 11z"/>
|
||||||
<circle cx="12" cy="10" r="2.3" stroke-width="1.8" />
|
<circle cx="12" cy="10" r="2.3" stroke-width="1.8" />
|
||||||
</svg>
|
</svg>
|
||||||
<span data-location-label class="max-w-40 truncate">Choose location</span>
|
<span data-location-label class="oc-location-label">Choose location</span>
|
||||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 9l6 6 6-6"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 9l6 6 6-6"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -118,18 +138,18 @@
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
@auth
|
@auth
|
||||||
<a href="{{ $favoritesRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Favorites">
|
<a href="{{ $favoritesRoute }}" class="header-utility oc-desktop-utility" aria-label="Favorites">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21l-1.45-1.32C5.4 15.03 2 12.01 2 8.31 2 5.3 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.08A6.04 6.04 0 0116.5 3C19.58 3 22 5.3 22 8.31c0 3.7-3.4 6.72-8.55 11.39L12 21z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21l-1.45-1.32C5.4 15.03 2 12.01 2 8.31 2 5.3 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.08A6.04 6.04 0 0116.5 3C19.58 3 22 5.3 22 8.31c0 3.7-3.4 6.72-8.55 11.39L12 21z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ $inboxRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Inbox">
|
<a href="{{ $inboxRoute }}" class="header-utility oc-desktop-utility" aria-label="Inbox">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6h16a1 1 0 011 1v10a1 1 0 01-1 1H4a1 1 0 01-1-1V7a1 1 0 011-1z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6h16a1 1 0 011 1v10a1 1 0 01-1 1H4a1 1 0 01-1-1V7a1 1 0 011-1z"/>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 8l9 6 9-6"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 8l9 6 9-6"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ $panelListingsRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Dashboard">
|
<a href="{{ $panelListingsRoute }}" class="header-utility oc-desktop-utility" aria-label="Dashboard">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 12l9-9 9 9M5 10v10h14V10"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 12l9-9 9 9M5 10v10h14V10"/>
|
||||||
</svg>
|
</svg>
|
||||||
@ -137,12 +157,12 @@
|
|||||||
<a href="{{ $panelCreateRoute }}" class="btn-primary oc-cta">
|
<a href="{{ $panelCreateRoute }}" class="btn-primary oc-cta">
|
||||||
Sell
|
Sell
|
||||||
</a>
|
</a>
|
||||||
<form method="POST" action="{{ $logoutRoute }}" class="hidden xl:block">
|
<form method="POST" action="{{ $logoutRoute }}" class="oc-logout">
|
||||||
@csrf
|
@csrf
|
||||||
<button type="submit" class="oc-text-link">{{ __('messages.logout') }}</button>
|
<button type="submit" class="oc-text-link">{{ __('messages.logout') }}</button>
|
||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
<a href="{{ $loginRoute }}" class="oc-text-link hidden md:inline-flex">
|
<a href="{{ $loginRoute }}" class="oc-text-link oc-auth-link">
|
||||||
{{ __('messages.login') }}
|
{{ __('messages.login') }}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ $panelCreateRoute }}" class="btn-primary oc-cta">
|
<a href="{{ $panelCreateRoute }}" class="btn-primary oc-cta">
|
||||||
@ -152,24 +172,94 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="oc-mobile-tools lg:hidden">
|
<div class="oc-mobile-menu-shell" id="oc-mobile-menu" data-mobile-menu>
|
||||||
<form action="{{ route('listings.index') }}" method="GET" class="oc-search">
|
<button type="button" class="oc-mobile-menu-backdrop" data-mobile-menu-close aria-label="Close navigation menu"></button>
|
||||||
<svg class="w-5 h-5 oc-search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M21 21l-4.35-4.35m1.6-5.05a7.25 7.25 0 11-14.5 0 7.25 7.25 0 0114.5 0z"/>
|
|
||||||
</svg>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="search"
|
|
||||||
value="{{ request('search') }}"
|
|
||||||
placeholder="{{ __('messages.search_placeholder') }}"
|
|
||||||
class="oc-search-input"
|
|
||||||
>
|
|
||||||
<button type="submit" class="oc-search-submit">{{ __('messages.search') }}</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="oc-mobile-pills">
|
<div class="oc-mobile-menu-panel" role="dialog" aria-modal="true" aria-label="Navigation menu">
|
||||||
<span class="oc-pill" data-location-label-mobile>Choose location</span>
|
<div class="oc-mobile-menu-header">
|
||||||
<a href="{{ $panelCreateRoute }}" class="oc-pill oc-pill-strong">Sell</a>
|
<h2 class="oc-mobile-menu-title">Menu</h2>
|
||||||
|
<button type="button" class="header-utility oc-mobile-menu-close" data-mobile-menu-close aria-label="Close navigation menu">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 6l12 12M18 6L6 18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="oc-mobile-menu-actions">
|
||||||
|
<a href="{{ route('listings.index') }}" class="oc-mobile-menu-primary">Browse</a>
|
||||||
|
<a href="{{ $panelCreateRoute }}" class="oc-mobile-menu-primary oc-mobile-menu-primary-strong">Sell</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="oc-mobile-menu-section">
|
||||||
|
<p class="oc-mobile-menu-label">Browse</p>
|
||||||
|
<div class="oc-mobile-menu-list">
|
||||||
|
@foreach($menuBrowseLinks as $menuBrowseLink)
|
||||||
|
<a href="{{ $menuBrowseLink['url'] }}" class="oc-mobile-menu-link">
|
||||||
|
<span>{{ $menuBrowseLink['label'] }}</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 6l6 6-6 6"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="oc-mobile-menu-section">
|
||||||
|
<p class="oc-mobile-menu-label">Account</p>
|
||||||
|
<div class="oc-mobile-menu-list">
|
||||||
|
@auth
|
||||||
|
<a href="{{ $panelListingsRoute }}" class="oc-mobile-menu-link">
|
||||||
|
<span>Dashboard</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 6l6 6-6 6"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="{{ $favoritesRoute }}" class="oc-mobile-menu-link">
|
||||||
|
<span>Favorites</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 6l6 6-6 6"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="{{ $inboxRoute }}" class="oc-mobile-menu-link">
|
||||||
|
<span>Inbox</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 6l6 6-6 6"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a href="{{ $loginRoute }}" class="oc-mobile-menu-link">
|
||||||
|
<span>Login</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 6l6 6-6 6"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="{{ $registerRoute }}" class="oc-mobile-menu-link">
|
||||||
|
<span>Register</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 6l6 6-6 6"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="oc-mobile-menu-section">
|
||||||
|
<p class="oc-mobile-menu-label">Languages</p>
|
||||||
|
<div class="oc-mobile-menu-languages">
|
||||||
|
@foreach($availableLocales as $locale)
|
||||||
|
<a href="{{ route('lang.switch', $locale) }}" class="oc-mobile-menu-language {{ app()->getLocale() === $locale ? 'is-active' : '' }}">
|
||||||
|
{{ $localeLabels[$locale] ?? strtoupper($locale) }}
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@auth
|
||||||
|
<form method="POST" action="{{ $logoutRoute }}" class="oc-mobile-menu-logout">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="oc-mobile-menu-logout-btn">Logout</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -259,10 +349,9 @@
|
|||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const widgetRoots = Array.from(document.querySelectorAll('[data-location-widget]'));
|
const widgetRoots = Array.from(document.querySelectorAll('[data-location-widget]'));
|
||||||
const mobileLabels = Array.from(document.querySelectorAll('[data-location-label-mobile]'));
|
|
||||||
const storageKey = 'oc2.header.location';
|
const storageKey = 'oc2.header.location';
|
||||||
|
|
||||||
if (widgetRoots.length === 0 && mobileLabels.length === 0) {
|
if (widgetRoots.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,9 +406,6 @@
|
|||||||
target.textContent = label;
|
target.textContent = label;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mobileLabels.forEach((target) => {
|
|
||||||
target.textContent = label;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchCityOptions = async (url) => {
|
const fetchCityOptions = async (url) => {
|
||||||
@ -696,6 +782,56 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const menu = document.querySelector('[data-mobile-menu]');
|
||||||
|
const openButtons = Array.from(document.querySelectorAll('[data-mobile-menu-open]'));
|
||||||
|
const closeButtons = Array.from(document.querySelectorAll('[data-mobile-menu-close]'));
|
||||||
|
|
||||||
|
if (!menu || openButtons.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setOpen = (shouldOpen) => {
|
||||||
|
menu.classList.toggle('is-open', shouldOpen);
|
||||||
|
document.documentElement.classList.toggle('oc-menu-open', shouldOpen);
|
||||||
|
document.body.style.overflow = shouldOpen ? 'hidden' : '';
|
||||||
|
|
||||||
|
openButtons.forEach((button) => {
|
||||||
|
button.setAttribute('aria-expanded', shouldOpen ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldOpen) {
|
||||||
|
document.querySelectorAll('[data-location-widget][open]').forEach((details) => {
|
||||||
|
details.removeAttribute('open');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openButtons.forEach((button) => {
|
||||||
|
button.addEventListener('click', () => setOpen(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
closeButtons.forEach((button) => {
|
||||||
|
button.addEventListener('click', () => setOpen(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.querySelectorAll('a').forEach((link) => {
|
||||||
|
link.addEventListener('click', () => setOpen(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (window.innerWidth >= 1024) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
<x-impersonate::banner />
|
<x-impersonate::banner />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
@extends('app::layouts.app')
|
@extends('app::layouts.app')
|
||||||
|
|
||||||
@section('title', 'Create Listing')
|
@section('title', 'New Listing')
|
||||||
|
|
||||||
@section('simple_page', '1')
|
@section('simple_page', '1')
|
||||||
|
|
||||||
|
|||||||
@ -1221,17 +1221,88 @@
|
|||||||
grid-template-columns: minmax(0, 1fr) 320px;
|
grid-template-columns: minmax(0, 1fr) 320px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.qc-hero {
|
||||||
|
gap: .7rem;
|
||||||
|
margin-bottom: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-eyebrow,
|
||||||
|
.qc-subtitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-title {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 1.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-step-label {
|
||||||
|
font-size: .82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-progress-wrap {
|
||||||
|
gap: .6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-card {
|
||||||
|
border-radius: .85rem;
|
||||||
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-body,
|
||||||
|
.qc-footer {
|
||||||
|
padding: .85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-upload-zone {
|
||||||
|
min-height: 176px;
|
||||||
|
padding: 1rem .85rem;
|
||||||
|
gap: .55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-upload-title,
|
||||||
|
.qc-ai-note h3,
|
||||||
|
.qc-photo-title {
|
||||||
|
font-size: 1.08rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-upload-desc,
|
||||||
|
.qc-help,
|
||||||
|
.qc-ai-note p {
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-help {
|
||||||
|
margin-top: .65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-photo-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: .55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-root-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: .85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qc-strip {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="qc-shell">
|
<div class="qc-shell">
|
||||||
<div class="qc-hero">
|
<div class="qc-hero">
|
||||||
<div class="qc-hero-copy">
|
<div class="qc-hero-copy">
|
||||||
<span class="qc-eyebrow">Create listing</span>
|
<span class="qc-eyebrow">New listing</span>
|
||||||
<h1 class="qc-title">{{ $this->currentStepTitle }}</h1>
|
<h1 class="qc-title">{{ $this->currentStepTitle }}</h1>
|
||||||
<p class="qc-subtitle">A clean, simple flow to publish faster.</p>
|
<p class="qc-subtitle">{{ $this->currentStepHint }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="qc-head">
|
<div class="qc-head">
|
||||||
<div class="qc-step-label">Step {{ $currentStep }} of 5</div>
|
<div class="qc-step-label">Step {{ $currentStep }}/5</div>
|
||||||
<div class="qc-progress-wrap">
|
<div class="qc-progress-wrap">
|
||||||
<div class="qc-progress" aria-hidden="true">
|
<div class="qc-progress" aria-hidden="true">
|
||||||
@for ($step = 1; $step <= 5; $step++)
|
@for ($step = 1; $step <= 5; $step++)
|
||||||
@ -1248,9 +1319,9 @@
|
|||||||
<div class="qc-body">
|
<div class="qc-body">
|
||||||
<label class="qc-upload-zone" for="quick-listing-photo-input">
|
<label class="qc-upload-zone" for="quick-listing-photo-input">
|
||||||
<x-heroicon-o-photo class="h-10 w-10 text-gray-700" />
|
<x-heroicon-o-photo class="h-10 w-10 text-gray-700" />
|
||||||
<div class="qc-upload-title">Start with photos</div>
|
<div class="qc-upload-title">Add photos</div>
|
||||||
<div class="qc-upload-desc">Add clear images first.</div>
|
<div class="qc-upload-desc">Clear photos work best.</div>
|
||||||
<span class="qc-upload-btn">Choose Photos</span>
|
<span class="qc-upload-btn">Select photos</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -1262,7 +1333,7 @@
|
|||||||
class="hidden"
|
class="hidden"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p class="qc-help">1 to {{ (int) config('quick-listing.max_photo_count', 20) }} images. JPG and PNG only.</p>
|
<p class="qc-help">1-{{ (int) config('quick-listing.max_photo_count', 20) }} photos. JPG or PNG.</p>
|
||||||
|
|
||||||
@error('photos')
|
@error('photos')
|
||||||
<div class="qc-error">{{ $message }}</div>
|
<div class="qc-error">{{ $message }}</div>
|
||||||
@ -1273,8 +1344,8 @@
|
|||||||
@enderror
|
@enderror
|
||||||
|
|
||||||
@if (count($photos) > 0)
|
@if (count($photos) > 0)
|
||||||
<h3 class="qc-photo-title">Selected photos</h3>
|
<h3 class="qc-photo-title">Your photos</h3>
|
||||||
<div class="qc-photo-sub">Drag to reorder</div>
|
<div class="qc-photo-sub">First photo is the cover</div>
|
||||||
|
|
||||||
<div class="qc-photo-grid">
|
<div class="qc-photo-grid">
|
||||||
@for ($index = 0; $index < (int) config('quick-listing.max_photo_count', 20); $index++)
|
@for ($index = 0; $index < (int) config('quick-listing.max_photo_count', 20); $index++)
|
||||||
@ -1294,8 +1365,8 @@
|
|||||||
@else
|
@else
|
||||||
<div class="qc-ai-note">
|
<div class="qc-ai-note">
|
||||||
<x-heroicon-o-sparkles class="h-10 w-10 text-pink-500" />
|
<x-heroicon-o-sparkles class="h-10 w-10 text-pink-500" />
|
||||||
<h3>Add at least one photo</h3>
|
<h3>Add one photo</h3>
|
||||||
<p>We can suggest a category after the first image.</p>
|
<p>We suggest a category after the first upload.</p>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@ -1307,7 +1378,7 @@
|
|||||||
wire:click="goToCategoryStep"
|
wire:click="goToCategoryStep"
|
||||||
@disabled(count($photos) === 0 || $isDetecting)
|
@disabled(count($photos) === 0 || $isDetecting)
|
||||||
>
|
>
|
||||||
Continue
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user