Add animations and fix publish
1
.gitignore
vendored
@ -28,3 +28,4 @@ Thumbs.db
|
|||||||
composer.lock
|
composer.lock
|
||||||
.codex/config.toml
|
.codex/config.toml
|
||||||
/public/vendor/
|
/public/vendor/
|
||||||
|
package-lock.json
|
||||||
|
|||||||
@ -9,25 +9,25 @@ class CategorySeeder extends Seeder
|
|||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$categories = [
|
$categories = [
|
||||||
['name' => 'Electronics', 'slug' => 'electronics', 'icon' => 'laptop', 'children' => ['Phones', 'Computers', 'Tablets', 'TVs']],
|
['name' => 'Electronics', 'slug' => 'electronics', 'icon' => 'img/category/electronics.png', 'children' => ['Phones', 'Computers', 'Tablets', 'TVs']],
|
||||||
['name' => 'Vehicles', 'slug' => 'vehicles', 'icon' => 'car', 'children' => ['Cars', 'Motorcycles', 'Trucks', 'Boats']],
|
['name' => 'Vehicles', 'slug' => 'vehicles', 'icon' => 'img/category/car.png', 'children' => ['Cars', 'Motorcycles', 'Trucks', 'Boats']],
|
||||||
['name' => 'Real Estate', 'slug' => 'real-estate', 'icon' => 'home', 'children' => ['For Sale', 'For Rent', 'Commercial']],
|
['name' => 'Real Estate', 'slug' => 'real-estate', 'icon' => 'img/category/home_garden.png', 'children' => ['For Sale', 'For Rent', 'Commercial']],
|
||||||
['name' => 'Fashion', 'slug' => 'fashion', 'icon' => 'shirt', 'children' => ['Men', 'Women', 'Kids', 'Shoes']],
|
['name' => 'Fashion', 'slug' => 'fashion', 'icon' => 'img/category/phone.png', 'children' => ['Men', 'Women', 'Kids', 'Shoes']],
|
||||||
['name' => 'Home & Garden', 'slug' => 'home-garden', 'icon' => 'sofa', 'children' => ['Furniture', 'Garden', 'Appliances']],
|
['name' => 'Home & Garden', 'slug' => 'home-garden', 'icon' => 'img/category/home_tools.png', 'children' => ['Furniture', 'Garden', 'Appliances']],
|
||||||
['name' => 'Sports', 'slug' => 'sports', 'icon' => 'football', 'children' => ['Outdoor', 'Fitness', 'Team Sports']],
|
['name' => 'Sports', 'slug' => 'sports', 'icon' => 'img/category/sports.png', 'children' => ['Outdoor', 'Fitness', 'Team Sports']],
|
||||||
['name' => 'Jobs', 'slug' => 'jobs', 'icon' => 'briefcase', 'children' => ['Full Time', 'Part Time', 'Freelance']],
|
['name' => 'Jobs', 'slug' => 'jobs', 'icon' => 'img/category/education.png', 'children' => ['Full Time', 'Part Time', 'Freelance']],
|
||||||
['name' => 'Services', 'slug' => 'services', 'icon' => 'wrench', 'children' => ['Cleaning', 'Repair', 'Education']],
|
['name' => 'Services', 'slug' => 'services', 'icon' => 'img/category/home_tools.png', 'children' => ['Cleaning', 'Repair', 'Education']],
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($categories as $index => $data) {
|
foreach ($categories as $index => $data) {
|
||||||
$parent = Category::firstOrCreate(
|
$parent = Category::updateOrCreate(
|
||||||
['slug' => $data['slug']],
|
['slug' => $data['slug']],
|
||||||
['name' => $data['name'], 'slug' => $data['slug'], 'icon' => $data['icon'], 'level' => 0, 'sort_order' => $index, 'is_active' => true]
|
['name' => $data['name'], 'slug' => $data['slug'], 'icon' => $data['icon'], 'level' => 0, 'sort_order' => $index, 'is_active' => true]
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ($data['children'] as $i => $childName) {
|
foreach ($data['children'] as $i => $childName) {
|
||||||
$childSlug = $data['slug'] . '-' . \Illuminate\Support\Str::slug($childName);
|
$childSlug = $data['slug'] . '-' . \Illuminate\Support\Str::slug($childName);
|
||||||
Category::firstOrCreate(
|
Category::updateOrCreate(
|
||||||
['slug' => $childSlug],
|
['slug' => $childSlug],
|
||||||
['name' => $childName, 'slug' => $childSlug, 'parent_id' => $parent->id, 'level' => 1, 'sort_order' => $i, 'is_active' => true]
|
['name' => $childName, 'slug' => $childSlug, 'parent_id' => $parent->id, 'level' => 1, 'sort_order' => $i, 'is_active' => true]
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,6 +14,23 @@ class Category extends Model
|
|||||||
{
|
{
|
||||||
use LogsActivity;
|
use LogsActivity;
|
||||||
|
|
||||||
|
private const ICON_PATHS = [
|
||||||
|
'car' => 'img/category/car.png',
|
||||||
|
'education' => 'img/category/education.png',
|
||||||
|
'electronics' => 'img/category/electronics.png',
|
||||||
|
'football' => 'img/category/sports.png',
|
||||||
|
'home' => 'img/category/home_garden.png',
|
||||||
|
'home-garden' => 'img/category/home_garden.png',
|
||||||
|
'home_garden' => 'img/category/home_garden.png',
|
||||||
|
'home-tools' => 'img/category/home_tools.png',
|
||||||
|
'home_tools' => 'img/category/home_tools.png',
|
||||||
|
'laptop' => 'img/category/laptop.png',
|
||||||
|
'mobile' => 'img/category/phone.png',
|
||||||
|
'pet' => 'img/category/pet.png',
|
||||||
|
'phone' => 'img/category/phone.png',
|
||||||
|
'sports' => 'img/category/sports.png',
|
||||||
|
];
|
||||||
|
|
||||||
protected $fillable = ['name', 'slug', 'description', 'icon', 'parent_id', 'level', 'sort_order', 'is_active'];
|
protected $fillable = ['name', 'slug', 'description', 'icon', 'parent_id', 'level', 'sort_order', 'is_active'];
|
||||||
protected $casts = ['is_active' => 'boolean'];
|
protected $casts = ['is_active' => 'boolean'];
|
||||||
|
|
||||||
@ -219,6 +236,32 @@ class Category extends Model
|
|||||||
return $this->hasMany(\Modules\Listing\Models\Listing::class)->where('status', 'active');
|
return $this->hasMany(\Modules\Listing\Models\Listing::class)->where('status', 'active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function resolvedIconPath(): ?string
|
||||||
|
{
|
||||||
|
$icon = trim((string) $this->icon);
|
||||||
|
|
||||||
|
if ($icon === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset(self::ICON_PATHS[$icon])) {
|
||||||
|
return self::ICON_PATHS[$icon];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/\.(png|jpg|jpeg|webp|svg)$/i', $icon) === 1) {
|
||||||
|
return ltrim($icon, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function iconUrl(): ?string
|
||||||
|
{
|
||||||
|
$path = $this->resolvedIconPath();
|
||||||
|
|
||||||
|
return $path ? asset($path) : null;
|
||||||
|
}
|
||||||
|
|
||||||
private static function buildListingDirectoryTree(Collection $categories, Collection $activeListingCounts, ?int $parentId = null): Collection
|
private static function buildListingDirectoryTree(Collection $categories, Collection $activeListingCounts, ?int $parentId = null): Collection
|
||||||
{
|
{
|
||||||
return $categories
|
return $categories
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Modules\Category\Database\Seeders;
|
|
||||||
|
|
||||||
use Illuminate\Database\Seeder;
|
|
||||||
use Modules\Category\Models\Category;
|
|
||||||
|
|
||||||
class CategorySeeder extends Seeder
|
|
||||||
{
|
|
||||||
public function run(): void
|
|
||||||
{
|
|
||||||
$categories = [
|
|
||||||
['name' => 'Electronics', 'slug' => 'electronics', 'icon' => 'laptop', 'children' => ['Phones', 'Computers', 'Tablets', 'TVs']],
|
|
||||||
['name' => 'Vehicles', 'slug' => 'vehicles', 'icon' => 'car', 'children' => ['Cars', 'Motorcycles', 'Trucks', 'Boats']],
|
|
||||||
['name' => 'Real Estate', 'slug' => 'real-estate', 'icon' => 'home', 'children' => ['For Sale', 'For Rent', 'Commercial']],
|
|
||||||
['name' => 'Fashion', 'slug' => 'fashion', 'icon' => 'shirt', 'children' => ['Men', 'Women', 'Kids', 'Shoes']],
|
|
||||||
['name' => 'Home & Garden', 'slug' => 'home-garden', 'icon' => 'sofa', 'children' => ['Furniture', 'Garden', 'Appliances']],
|
|
||||||
['name' => 'Sports', 'slug' => 'sports', 'icon' => 'football', 'children' => ['Outdoor', 'Fitness', 'Team Sports']],
|
|
||||||
['name' => 'Jobs', 'slug' => 'jobs', 'icon' => 'briefcase', 'children' => ['Full Time', 'Part Time', 'Freelance']],
|
|
||||||
['name' => 'Services', 'slug' => 'services', 'icon' => 'wrench', 'children' => ['Cleaning', 'Repair', 'Education']],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($categories as $index => $data) {
|
|
||||||
$parent = Category::firstOrCreate(
|
|
||||||
['slug' => $data['slug']],
|
|
||||||
['name' => $data['name'], 'slug' => $data['slug'], 'icon' => $data['icon'], 'level' => 0, 'sort_order' => $index, 'is_active' => true]
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ($data['children'] as $i => $childName) {
|
|
||||||
$childSlug = $data['slug'] . '-' . \Illuminate\Support\Str::slug($childName);
|
|
||||||
Category::firstOrCreate(
|
|
||||||
['slug' => $childSlug],
|
|
||||||
['name' => $childName, 'slug' => $childSlug, 'parent_id' => $parent->id, 'level' => 1, 'sort_order' => $i, 'is_active' => true]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -21,17 +21,7 @@
|
|||||||
->filter()
|
->filter()
|
||||||
->implode(' · ');
|
->implode(' · ');
|
||||||
$extraChildCount = max($category->children->count() - 3, 0);
|
$extraChildCount = max($category->children->count() - 3, 0);
|
||||||
$icon = match (trim((string) ($category->icon ?? ''))) {
|
$iconUrl = $category->iconUrl();
|
||||||
'laptop' => 'heroicon-o-computer-desktop',
|
|
||||||
'car' => 'heroicon-o-truck',
|
|
||||||
'home' => 'heroicon-o-home',
|
|
||||||
'shirt' => 'heroicon-o-shopping-bag',
|
|
||||||
'sofa' => 'heroicon-o-home-modern',
|
|
||||||
'football' => 'heroicon-o-trophy',
|
|
||||||
'briefcase' => 'heroicon-o-briefcase',
|
|
||||||
'wrench' => 'heroicon-o-wrench-screwdriver',
|
|
||||||
default => null,
|
|
||||||
};
|
|
||||||
$iconLabel = strtoupper(\Illuminate\Support\Str::substr($category->name, 0, 1));
|
$iconLabel = strtoupper(\Illuminate\Support\Str::substr($category->name, 0, 1));
|
||||||
@endphp
|
@endphp
|
||||||
<a
|
<a
|
||||||
@ -39,11 +29,11 @@
|
|||||||
class="group flex h-full flex-col rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm transition duration-200 hover:-translate-y-0.5 hover:border-blue-200 hover:shadow-lg"
|
class="group flex h-full flex-col rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm transition duration-200 hover:-translate-y-0.5 hover:border-blue-200 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<span class="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-slate-200 bg-slate-50 text-slate-900 shadow-sm">
|
<span class="flex h-16 w-16 shrink-0 items-center justify-center rounded-2xl border border-slate-200 bg-slate-50 text-slate-900 shadow-sm">
|
||||||
@if($icon)
|
@if($iconUrl)
|
||||||
<x-dynamic-component :component="$icon" class="h-7 w-7" />
|
<img src="{{ $iconUrl }}" alt="{{ $category->name }}" class="h-11 w-11 object-contain">
|
||||||
@else
|
@else
|
||||||
<span class="text-2xl font-semibold">{{ $iconLabel }}</span>
|
<span class="text-3xl font-semibold">{{ $iconLabel }}</span>
|
||||||
@endif
|
@endif
|
||||||
</span>
|
</span>
|
||||||
<span class="inline-flex items-center rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-semibold text-slate-600">
|
<span class="inline-flex items-center rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-semibold text-slate-600">
|
||||||
|
|||||||
@ -7,6 +7,7 @@ use Illuminate\Support\Collection;
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
use Livewire\Features\SupportFileUploads\WithFileUploads;
|
use Livewire\Features\SupportFileUploads\WithFileUploads;
|
||||||
@ -27,6 +28,7 @@ class PanelQuickListingForm extends Component
|
|||||||
use WithFileUploads;
|
use WithFileUploads;
|
||||||
|
|
||||||
private const TOTAL_STEPS = 5;
|
private const TOTAL_STEPS = 5;
|
||||||
|
private const DRAFT_SESSION_KEY = 'panel_quick_listing_draft';
|
||||||
|
|
||||||
public array $photos = [];
|
public array $photos = [];
|
||||||
public array $videos = [];
|
public array $videos = [];
|
||||||
@ -54,12 +56,14 @@ class PanelQuickListingForm extends Component
|
|||||||
public ?int $selectedCountryId = null;
|
public ?int $selectedCountryId = null;
|
||||||
public ?int $selectedCityId = null;
|
public ?int $selectedCityId = null;
|
||||||
public bool $isPublishing = false;
|
public bool $isPublishing = false;
|
||||||
|
public bool $shouldPersistDraft = true;
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->loadCategories();
|
$this->loadCategories();
|
||||||
$this->loadLocations();
|
$this->loadLocations();
|
||||||
$this->hydrateLocationDefaultsFromProfile();
|
$this->hydrateLocationDefaultsFromProfile();
|
||||||
|
$this->restoreDraft();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
@ -67,6 +71,15 @@ class PanelQuickListingForm extends Component
|
|||||||
return view('panel.quick-create');
|
return view('panel.quick-create');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function dehydrate(): void
|
||||||
|
{
|
||||||
|
if (! $this->shouldPersistDraft) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->persistDraft();
|
||||||
|
}
|
||||||
|
|
||||||
public function updatedPhotos(): void
|
public function updatedPhotos(): void
|
||||||
{
|
{
|
||||||
$this->validatePhotos();
|
$this->validatePhotos();
|
||||||
@ -198,14 +211,18 @@ class PanelQuickListingForm extends Component
|
|||||||
|
|
||||||
$this->isPublishing = true;
|
$this->isPublishing = true;
|
||||||
|
|
||||||
$this->validatePhotos();
|
|
||||||
$this->validateVideos();
|
|
||||||
$this->validateCategoryStep();
|
|
||||||
$this->validateDetailsStep();
|
|
||||||
$this->validateCustomFieldsStep();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
$this->validatePhotos();
|
||||||
|
$this->validateVideos();
|
||||||
|
$this->validateCategoryStep();
|
||||||
|
$this->validateDetailsStep();
|
||||||
|
$this->validateCustomFieldsStep();
|
||||||
|
|
||||||
$listing = $this->createListing();
|
$listing = $this->createListing();
|
||||||
|
} catch (ValidationException $exception) {
|
||||||
|
$this->isPublishing = false;
|
||||||
|
|
||||||
|
throw $exception;
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
report($exception);
|
report($exception);
|
||||||
$this->isPublishing = false;
|
$this->isPublishing = false;
|
||||||
@ -216,9 +233,10 @@ class PanelQuickListingForm extends Component
|
|||||||
|
|
||||||
$this->isPublishing = false;
|
$this->isPublishing = false;
|
||||||
session()->flash('success', 'Your listing has been created successfully.');
|
session()->flash('success', 'Your listing has been created successfully.');
|
||||||
|
$this->clearDraft();
|
||||||
|
|
||||||
if (Route::has('panel.listings.edit')) {
|
if (Route::has('panel.listings.edit')) {
|
||||||
$this->redirect(route('panel.listings.edit', $listing), navigate: true);
|
$this->redirectRoute('panel.listings.edit', ['listing' => $listing->getKey()]);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -754,6 +772,69 @@ class PanelQuickListingForm extends Component
|
|||||||
return (string) config('media_storage.local_disk', MediaStorage::diskFromDriver(MediaStorage::DRIVER_LOCAL));
|
return (string) config('media_storage.local_disk', MediaStorage::diskFromDriver(MediaStorage::DRIVER_LOCAL));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function restoreDraft(): void
|
||||||
|
{
|
||||||
|
$draft = session()->get($this->draftSessionKey(), []);
|
||||||
|
|
||||||
|
if (! is_array($draft) || $draft === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->currentStep = max(1, min(self::TOTAL_STEPS, (int) ($draft['currentStep'] ?? 1)));
|
||||||
|
$this->categorySearch = (string) ($draft['categorySearch'] ?? '');
|
||||||
|
$this->selectedCategoryId = isset($draft['selectedCategoryId']) ? (int) $draft['selectedCategoryId'] : null;
|
||||||
|
$this->activeParentCategoryId = isset($draft['activeParentCategoryId']) ? (int) $draft['activeParentCategoryId'] : null;
|
||||||
|
$this->detectedCategoryId = isset($draft['detectedCategoryId']) ? (int) $draft['detectedCategoryId'] : null;
|
||||||
|
$this->detectedConfidence = isset($draft['detectedConfidence']) ? (float) $draft['detectedConfidence'] : null;
|
||||||
|
$this->detectedReason = isset($draft['detectedReason']) ? (string) $draft['detectedReason'] : null;
|
||||||
|
$this->detectedError = isset($draft['detectedError']) ? (string) $draft['detectedError'] : null;
|
||||||
|
$this->detectedAlternatives = collect($draft['detectedAlternatives'] ?? [])->filter(fn ($id) => is_numeric($id))->map(fn ($id) => (int) $id)->values()->all();
|
||||||
|
$this->listingTitle = (string) ($draft['listingTitle'] ?? '');
|
||||||
|
$this->price = (string) ($draft['price'] ?? '');
|
||||||
|
$this->description = (string) ($draft['description'] ?? '');
|
||||||
|
$this->selectedCountryId = isset($draft['selectedCountryId']) ? (int) $draft['selectedCountryId'] : $this->selectedCountryId;
|
||||||
|
$this->selectedCityId = isset($draft['selectedCityId']) ? (int) $draft['selectedCityId'] : null;
|
||||||
|
$this->customFieldValues = is_array($draft['customFieldValues'] ?? null) ? $draft['customFieldValues'] : [];
|
||||||
|
|
||||||
|
if ($this->selectedCategoryId) {
|
||||||
|
$this->loadListingCustomFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistDraft(): void
|
||||||
|
{
|
||||||
|
session()->put($this->draftSessionKey(), [
|
||||||
|
'currentStep' => $this->currentStep,
|
||||||
|
'categorySearch' => $this->categorySearch,
|
||||||
|
'selectedCategoryId' => $this->selectedCategoryId,
|
||||||
|
'activeParentCategoryId' => $this->activeParentCategoryId,
|
||||||
|
'detectedCategoryId' => $this->detectedCategoryId,
|
||||||
|
'detectedConfidence' => $this->detectedConfidence,
|
||||||
|
'detectedReason' => $this->detectedReason,
|
||||||
|
'detectedError' => $this->detectedError,
|
||||||
|
'detectedAlternatives' => $this->detectedAlternatives,
|
||||||
|
'listingTitle' => $this->listingTitle,
|
||||||
|
'price' => $this->price,
|
||||||
|
'description' => $this->description,
|
||||||
|
'selectedCountryId' => $this->selectedCountryId,
|
||||||
|
'selectedCityId' => $this->selectedCityId,
|
||||||
|
'customFieldValues' => $this->customFieldValues,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function clearDraft(): void
|
||||||
|
{
|
||||||
|
$this->shouldPersistDraft = false;
|
||||||
|
session()->forget($this->draftSessionKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function draftSessionKey(): string
|
||||||
|
{
|
||||||
|
$userId = auth()->id() ?: 'guest';
|
||||||
|
|
||||||
|
return self::DRAFT_SESSION_KEY.'.'.$userId;
|
||||||
|
}
|
||||||
|
|
||||||
private function categoryPathParts(int $categoryId): array
|
private function categoryPathParts(int $categoryId): array
|
||||||
{
|
{
|
||||||
$byId = collect($this->categories)->keyBy('id');
|
$byId = collect($this->categories)->keyBy('id');
|
||||||
|
|||||||
@ -201,10 +201,11 @@ final class RequestAppData
|
|||||||
->orderBy('sort_order')
|
->orderBy('sort_order')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->limit(8)
|
->limit(8)
|
||||||
->get(['id', 'name'])
|
->get(['id', 'name', 'icon'])
|
||||||
->map(fn (Category $category): array => [
|
->map(fn (Category $category): array => [
|
||||||
'id' => (int) $category->id,
|
'id' => (int) $category->id,
|
||||||
'name' => (string) $category->name,
|
'name' => (string) $category->name,
|
||||||
|
'icon_url' => $category->iconUrl(),
|
||||||
])
|
])
|
||||||
->values()
|
->values()
|
||||||
->all();
|
->all();
|
||||||
|
|||||||
@ -17,5 +17,8 @@
|
|||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"tailwindcss": "^3.1.0",
|
"tailwindcss": "^3.1.0",
|
||||||
"vite": "^7.0.7"
|
"vite": "^7.0.7"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"animejs": "^4.3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/img/category/car.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/img/category/education.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
public/img/category/electronics.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
public/img/category/home_garden.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
public/img/category/home_tools.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
public/img/category/laptop.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/img/category/pet.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/img/category/phone.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/img/category/sports.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@ -86,12 +86,14 @@ h6 {
|
|||||||
background: rgba(251, 251, 253, 0.88);
|
background: rgba(251, 251, 253, 0.88);
|
||||||
backdrop-filter: saturate(180%) blur(18px);
|
backdrop-filter: saturate(180%) blur(18px);
|
||||||
border-bottom: 1px solid var(--oc-border);
|
border-bottom: 1px solid var(--oc-border);
|
||||||
|
overflow-x: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-nav-wrap {
|
.oc-nav-wrap {
|
||||||
max-width: 1320px;
|
max-width: 1320px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 10px 16px 12px;
|
padding: 10px 16px 12px;
|
||||||
|
overflow-x: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-nav-main {
|
.oc-nav-main {
|
||||||
@ -454,6 +456,7 @@ h6 {
|
|||||||
.oc-category-link {
|
.oc-category-link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
padding: 0 18px;
|
padding: 0 18px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@ -464,6 +467,21 @@ h6 {
|
|||||||
transition: background 0.2s ease, color 0.2s ease;
|
transition: background 0.2s ease, color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oc-category-link-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-category-link-icon img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.oc-category-link:hover,
|
.oc-category-link:hover,
|
||||||
.oc-category-pill:hover,
|
.oc-category-pill:hover,
|
||||||
.oc-pill:hover {
|
.oc-pill:hover {
|
||||||
|
|||||||
@ -1 +1,176 @@
|
|||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
|
import { animate, createTimeline, stagger } from 'animejs';
|
||||||
|
|
||||||
|
const prefersReducedMotion = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
const onReady = (callback) => {
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', callback, { once: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateHeader = () => {
|
||||||
|
const nav = document.querySelector('[data-anim-nav]');
|
||||||
|
const categoryRow = document.querySelector('[data-anim-header-row]');
|
||||||
|
|
||||||
|
if (nav) {
|
||||||
|
animate(nav, {
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateY: [-18, 0],
|
||||||
|
duration: 700,
|
||||||
|
ease: 'outExpo',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryRow) {
|
||||||
|
animate(categoryRow.querySelectorAll('.oc-category-pill, .oc-category-link'), {
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateY: [-10, 0],
|
||||||
|
delay: stagger(45, { start: 140 }),
|
||||||
|
duration: 520,
|
||||||
|
ease: 'outQuad',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateHomeHero = () => {
|
||||||
|
const hero = document.querySelector('[data-home-hero]');
|
||||||
|
|
||||||
|
if (!hero) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = hero.querySelector('[data-home-hero-copy]');
|
||||||
|
const visual = hero.querySelector('[data-home-hero-visual]');
|
||||||
|
const timeline = createTimeline({
|
||||||
|
defaults: {
|
||||||
|
duration: 720,
|
||||||
|
ease: 'outExpo',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (copy) {
|
||||||
|
timeline.add(copy.querySelectorAll('[data-home-slide] > *'), {
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateY: [28, 0],
|
||||||
|
delay: stagger(80),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visual) {
|
||||||
|
timeline.add(visual, {
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateX: [36, 0],
|
||||||
|
scale: [0.96, 1],
|
||||||
|
}, '<+=120');
|
||||||
|
|
||||||
|
animate(visual, {
|
||||||
|
translateY: [
|
||||||
|
{ to: -8, duration: 2800, ease: 'inOutSine' },
|
||||||
|
{ to: 0, duration: 2800, ease: 'inOutSine' },
|
||||||
|
],
|
||||||
|
loop: true,
|
||||||
|
alternate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateOnView = (selector, itemSelector, options = {}) => {
|
||||||
|
const sections = document.querySelectorAll(selector);
|
||||||
|
|
||||||
|
if (!sections.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry.isIntersecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const section = entry.target;
|
||||||
|
const items = itemSelector ? section.querySelectorAll(itemSelector) : [section];
|
||||||
|
|
||||||
|
animate(items, {
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateY: [24, 0],
|
||||||
|
delay: stagger(options.stagger ?? 70),
|
||||||
|
duration: options.duration ?? 650,
|
||||||
|
ease: options.ease ?? 'outExpo',
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.unobserve(section);
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
threshold: 0.18,
|
||||||
|
rootMargin: '0px 0px -8% 0px',
|
||||||
|
});
|
||||||
|
|
||||||
|
sections.forEach((section) => observer.observe(section));
|
||||||
|
};
|
||||||
|
|
||||||
|
const bindHoverLift = (selector, distance = 6) => {
|
||||||
|
document.querySelectorAll(selector).forEach((element) => {
|
||||||
|
element.addEventListener('mouseenter', () => {
|
||||||
|
animate(element, {
|
||||||
|
translateY: -distance,
|
||||||
|
scale: 1.015,
|
||||||
|
duration: 260,
|
||||||
|
ease: 'outQuad',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
element.addEventListener('mouseleave', () => {
|
||||||
|
animate(element, {
|
||||||
|
translateY: 0,
|
||||||
|
scale: 1,
|
||||||
|
duration: 320,
|
||||||
|
ease: 'outExpo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const animateFooter = () => {
|
||||||
|
const footer = document.querySelector('[data-anim-footer]');
|
||||||
|
|
||||||
|
if (!footer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (!entry.isIntersecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
animate(footer.querySelectorAll('[data-anim-footer-item]'), {
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateY: [26, 0],
|
||||||
|
delay: stagger(90),
|
||||||
|
duration: 620,
|
||||||
|
ease: 'outExpo',
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
}, { threshold: 0.2 });
|
||||||
|
|
||||||
|
observer.observe(footer);
|
||||||
|
};
|
||||||
|
|
||||||
|
onReady(() => {
|
||||||
|
if (prefersReducedMotion()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
animateHeader();
|
||||||
|
animateHomeHero();
|
||||||
|
animateOnView('[data-home-section]', '[data-home-category-card], [data-home-listing-card], h2, a, article, section > div');
|
||||||
|
animateFooter();
|
||||||
|
bindHoverLift('[data-home-category-card]', 5);
|
||||||
|
bindHoverLift('[data-home-listing-card]', 4);
|
||||||
|
});
|
||||||
|
|||||||
@ -58,26 +58,6 @@
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$trendSkins = [
|
|
||||||
['gradient' => 'from-emerald-800 via-emerald-700 to-emerald-600', 'glow' => 'bg-emerald-200/45'],
|
|
||||||
['gradient' => 'from-rose-700 via-rose-600 to-pink-500', 'glow' => 'bg-rose-200/40'],
|
|
||||||
['gradient' => 'from-rose-700 via-pink-600 to-fuchsia-500', 'glow' => 'bg-pink-200/40'],
|
|
||||||
['gradient' => 'from-rose-700 via-rose-600 to-orange-500', 'glow' => 'bg-orange-200/40'],
|
|
||||||
['gradient' => 'from-rose-700 via-pink-600 to-red-500', 'glow' => 'bg-rose-200/40'],
|
|
||||||
['gradient' => 'from-fuchsia-700 via-pink-600 to-rose-500', 'glow' => 'bg-fuchsia-200/40'],
|
|
||||||
['gradient' => 'from-rose-700 via-rose-600 to-pink-500', 'glow' => 'bg-rose-200/40'],
|
|
||||||
['gradient' => 'from-red-700 via-rose-600 to-pink-500', 'glow' => 'bg-red-200/40'],
|
|
||||||
];
|
|
||||||
$trendIcons = [
|
|
||||||
'gift',
|
|
||||||
'computer',
|
|
||||||
'bike',
|
|
||||||
'sparkles',
|
|
||||||
'coffee',
|
|
||||||
'laptop',
|
|
||||||
'fitness',
|
|
||||||
'game',
|
|
||||||
];
|
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@if($demoLandingMode && $prepareDemoRoute)
|
@if($demoLandingMode && $prepareDemoRoute)
|
||||||
@ -99,11 +79,11 @@
|
|||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-5 md:py-7 space-y-7">
|
<div class="max-w-[1320px] mx-auto px-4 py-5 md:py-7 space-y-7">
|
||||||
<section class="relative overflow-hidden rounded-[28px] bg-gradient-to-r from-blue-900 via-blue-700 to-blue-600 text-white shadow-xl">
|
<section class="relative overflow-hidden rounded-[28px] bg-gradient-to-r from-blue-900 via-blue-700 to-blue-600 text-white shadow-xl" data-home-hero>
|
||||||
<div class="absolute -top-20 -left-24 w-80 h-80 rounded-full bg-blue-400/20 blur-3xl"></div>
|
<div class="absolute -top-20 -left-24 w-80 h-80 rounded-full bg-blue-400/20 blur-3xl"></div>
|
||||||
<div class="absolute -bottom-24 right-10 w-80 h-80 rounded-full bg-cyan-300/20 blur-3xl"></div>
|
<div class="absolute -bottom-24 right-10 w-80 h-80 rounded-full bg-cyan-300/20 blur-3xl"></div>
|
||||||
<div class="relative grid lg:grid-cols-[1fr,1.1fr] gap-6 items-center px-8 md:px-12 py-12 md:py-14">
|
<div class="relative grid lg:grid-cols-[1fr,1.1fr] gap-6 items-center px-8 md:px-12 py-12 md:py-14">
|
||||||
<div data-home-slider>
|
<div data-home-slider data-home-hero-copy>
|
||||||
<div class="relative min-h-[250px]">
|
<div class="relative min-h-[250px]">
|
||||||
@foreach($homeSlides as $index => $slide)
|
@foreach($homeSlides as $index => $slide)
|
||||||
<div
|
<div
|
||||||
@ -169,7 +149,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="relative h-[310px] md:h-[360px]">
|
<div class="relative h-[310px] md:h-[360px]" data-home-hero-visual>
|
||||||
<div class="absolute left-6 md:left-10 bottom-0 w-32 md:w-40 h-[250px] md:h-[300px] bg-slate-950 rounded-[32px] shadow-2xl p-2 rotate-[-8deg]">
|
<div class="absolute left-6 md:left-10 bottom-0 w-32 md:w-40 h-[250px] md:h-[300px] bg-slate-950 rounded-[32px] shadow-2xl p-2 rotate-[-8deg]">
|
||||||
<div class="w-full h-full rounded-[24px] bg-white overflow-hidden">
|
<div class="w-full h-full rounded-[24px] bg-white overflow-hidden">
|
||||||
<div class="px-3 py-2 border-b border-slate-100">
|
<div class="px-3 py-2 border-b border-slate-100">
|
||||||
@ -214,7 +194,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section data-home-section>
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<h2 class="text-3xl font-extrabold tracking-tight text-slate-900">Trending Categories</h2>
|
<h2 class="text-3xl font-extrabold tracking-tight text-slate-900">Trending Categories</h2>
|
||||||
<a href="{{ route('categories.index') }}" class="hidden sm:inline-flex text-sm font-semibold text-rose-500 hover:text-rose-600 transition">
|
<a href="{{ route('categories.index') }}" class="hidden sm:inline-flex text-sm font-semibold text-rose-500 hover:text-rose-600 transition">
|
||||||
@ -235,59 +215,19 @@
|
|||||||
<div data-trend-track class="flex items-stretch gap-2 overflow-x-auto pb-2 pr-1 scroll-smooth snap-x snap-mandatory [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
<div data-trend-track class="flex items-stretch gap-2 overflow-x-auto pb-2 pr-1 scroll-smooth snap-x snap-mandatory [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||||
@foreach($menuCategories as $index => $category)
|
@foreach($menuCategories as $index => $category)
|
||||||
@php
|
@php
|
||||||
$trendSkin = $trendSkins[$index % count($trendSkins)];
|
$categoryIconUrl = $category->iconUrl();
|
||||||
$trendIcon = $trendIcons[$index % count($trendIcons)];
|
$fallbackLabel = strtoupper(\Illuminate\Support\Str::substr($category->name, 0, 1));
|
||||||
@endphp
|
@endphp
|
||||||
<a href="{{ route('listings.index', ['category' => $category->id]) }}" class="group shrink-0 w-[170px] rounded-xl overflow-hidden border border-slate-300/80 bg-white hover:shadow-md transition snap-start">
|
<a href="{{ route('listings.index', ['category' => $category->id]) }}" class="group shrink-0 w-[170px] rounded-[22px] overflow-hidden border border-slate-200/80 bg-white/95 p-4 hover:-translate-y-0.5 hover:shadow-[0_18px_36px_rgba(15,23,42,0.08)] transition snap-start" data-home-category-card>
|
||||||
<div class="h-[68px] bg-gradient-to-r {{ $trendSkin['gradient'] }} relative overflow-hidden">
|
<div class="flex items-center justify-center h-[92px] rounded-[20px] bg-[linear-gradient(180deg,#f8fbff_0%,#eef5ff_100%)]">
|
||||||
<span class="absolute -left-5 top-2 w-20 h-20 rounded-full {{ $trendSkin['glow'] }} blur-2xl"></span>
|
@if($categoryIconUrl)
|
||||||
<span class="absolute left-5 bottom-2 h-2.5 w-24 rounded-full bg-black/20"></span>
|
<img src="{{ $categoryIconUrl }}" alt="{{ $category->name }}" class="h-14 w-14 object-contain">
|
||||||
<span class="absolute right-3 bottom-2 w-11 h-11 rounded-lg border border-white/35 bg-white/10 backdrop-blur-sm text-white grid place-items-center shadow-sm">
|
@else
|
||||||
@switch($trendIcon)
|
<span class="inline-flex h-14 w-14 items-center justify-center rounded-full bg-white text-xl font-semibold text-slate-700 shadow-sm">{{ $fallbackLabel }}</span>
|
||||||
@case('gift')
|
@endif
|
||||||
<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="M20 12v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-8m16 0H4m16 0V8a1 1 0 0 0-1-1h-3.5M4 12V8a1 1 0 0 1 1-1h3.5m0 0a2 2 0 1 1 0-4c2.5 0 3.5 4 3.5 4m-3.5 0h7m0 0a2 2 0 1 0 0-4c-2.5 0-3.5 4-3.5 4"/>
|
|
||||||
</svg>
|
|
||||||
@break
|
|
||||||
@case('computer')
|
|
||||||
<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 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm4 16h10m-8 0 1.5-3m4 3L13 18"/>
|
|
||||||
</svg>
|
|
||||||
@break
|
|
||||||
@case('bike')
|
|
||||||
<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="M5.5 17.5A3.5 3.5 0 1 0 5.5 10a3.5 3.5 0 0 0 0 7.5Zm13 0A3.5 3.5 0 1 0 18.5 10a3.5 3.5 0 0 0 0 7.5ZM8.5 14l3-5h3l2 5m-5-5L9 6h3"/>
|
|
||||||
</svg>
|
|
||||||
@break
|
|
||||||
@case('sparkles')
|
|
||||||
<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="m9 5 1.4 2.6L13 9l-2.6 1.4L9 13l-1.4-2.6L5 9l2.6-1.4L9 5Zm8 4 1 1.9L20 12l-2 1.1-1 1.9-1-1.9-2-1.1 2-1.1L17 9ZM7 16l1.5 2.8L11.5 20 8.5 21.2 7 24l-1.5-2.8L2.5 20l3-1.2L7 16Z"/>
|
|
||||||
</svg>
|
|
||||||
@break
|
|
||||||
@case('coffee')
|
|
||||||
<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="M7 8v7a3 3 0 0 0 3 3h4a3 3 0 0 0 3-3V8H7Zm10 2h1a2 2 0 1 1 0 4h-1M8 4v2m4-2v2m4-2v2M6 21h12"/>
|
|
||||||
</svg>
|
|
||||||
@break
|
|
||||||
@case('laptop')
|
|
||||||
<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 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8H4V6Zm-1 10h18l-1.2 3H4.2L3 16Z"/>
|
|
||||||
</svg>
|
|
||||||
@break
|
|
||||||
@case('fitness')
|
|
||||||
<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 10h2v4H3v-4Zm16 0h2v4h-2v-4ZM7 8h2v8H7V8Zm8 0h2v8h-2V8Zm-4 2h2v4h-2v-4Z"/>
|
|
||||||
</svg>
|
|
||||||
@break
|
|
||||||
@default
|
|
||||||
<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="M15 6h3a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3m-6 0H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3 4h.01m-1 8h2a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2Z"/>
|
|
||||||
</svg>
|
|
||||||
@endswitch
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="px-3 py-2.5">
|
<div class="pt-4">
|
||||||
<p class="text-[12px] sm:text-[13px] font-semibold text-slate-900 leading-tight truncate">{{ $category->name }}</p>
|
<p class="text-[13px] sm:text-[14px] font-semibold text-slate-900 leading-tight">{{ $category->name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
@ -305,7 +245,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section data-home-section>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-2xl font-bold text-slate-900">Popular Listings</h2>
|
<h2 class="text-2xl font-bold text-slate-900">Popular Listings</h2>
|
||||||
<div class="hidden sm:flex items-center gap-2 text-sm text-slate-500">
|
<div class="hidden sm:flex items-center gap-2 text-sm text-slate-500">
|
||||||
@ -321,7 +261,7 @@
|
|||||||
$locationLabel = trim(collect([$listing->city, $listing->country])->filter()->join(', '));
|
$locationLabel = trim(collect([$listing->city, $listing->country])->filter()->join(', '));
|
||||||
$isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
|
$isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
|
||||||
@endphp
|
@endphp
|
||||||
<article class="rounded-2xl border border-slate-200 bg-white overflow-hidden shadow-sm hover:shadow-md transition">
|
<article class="rounded-2xl border border-slate-200 bg-white overflow-hidden shadow-sm hover:shadow-md transition" data-home-listing-card>
|
||||||
<div class="relative h-64 md:h-[290px] bg-slate-100">
|
<div class="relative h-64 md:h-[290px] bg-slate-100">
|
||||||
<a href="{{ route('listings.show', $listing) }}" class="block h-full w-full" aria-label="{{ $listing->title }}">
|
<a href="{{ route('listings.show', $listing) }}" class="block h-full w-full" aria-label="{{ $listing->title }}">
|
||||||
@if($listingImage)
|
@if($listingImage)
|
||||||
@ -373,7 +313,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rounded-3xl bg-slate-900 text-white px-8 py-10 md:p-12">
|
<section class="rounded-3xl bg-slate-900 text-white px-8 py-10 md:p-12" data-home-section>
|
||||||
<div class="grid md:grid-cols-[1fr,auto] gap-6 items-center">
|
<div class="grid md:grid-cols-[1fr,auto] gap-6 items-center">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-3xl md:text-4xl font-extrabold">{{ __('messages.sell_something') }}</h2>
|
<h2 class="text-3xl md:text-4xl font-extrabold">{{ __('messages.sell_something') }}</h2>
|
||||||
|
|||||||
@ -103,7 +103,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@else
|
@else
|
||||||
<nav class="market-nav-surface sticky top-0 z-50">
|
<nav class="market-nav-surface sticky top-0 z-50" data-anim-nav>
|
||||||
<div class="oc-nav-wrap">
|
<div class="oc-nav-wrap">
|
||||||
<div class="oc-nav-main">
|
<div class="oc-nav-main">
|
||||||
<div class="oc-topbar">
|
<div class="oc-topbar">
|
||||||
@ -316,7 +316,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="oc-category-row">
|
<div class="oc-category-row" data-anim-header-row>
|
||||||
<div class="oc-category-track">
|
<div class="oc-category-track">
|
||||||
<a href="{{ route('categories.index') }}" class="oc-category-pill">
|
<a href="{{ route('categories.index') }}" class="oc-category-pill">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
@ -326,6 +326,11 @@
|
|||||||
</a>
|
</a>
|
||||||
@forelse($headerCategories as $headerCategory)
|
@forelse($headerCategories as $headerCategory)
|
||||||
<a href="{{ route('listings.index', ['category' => $headerCategory['id']]) }}" class="oc-category-link">
|
<a href="{{ route('listings.index', ['category' => $headerCategory['id']]) }}" class="oc-category-link">
|
||||||
|
@if(! empty($headerCategory['icon_url']))
|
||||||
|
<span class="oc-category-link-icon">
|
||||||
|
<img src="{{ $headerCategory['icon_url'] }}" alt="{{ $headerCategory['name'] }}">
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
{{ $headerCategory['name'] }}
|
{{ $headerCategory['name'] }}
|
||||||
</a>
|
</a>
|
||||||
@empty
|
@empty
|
||||||
@ -359,14 +364,14 @@
|
|||||||
'min-h-screen' => $demoLandingMode,
|
'min-h-screen' => $demoLandingMode,
|
||||||
])>@yield('content')</main>
|
])>@yield('content')</main>
|
||||||
@if(!$simplePage)
|
@if(!$simplePage)
|
||||||
<footer class="mt-14 bg-slate-100 text-slate-600 border-t border-slate-200">
|
<footer class="mt-14 bg-slate-100 text-slate-600 border-t border-slate-200" data-anim-footer>
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-12">
|
<div class="max-w-[1320px] mx-auto px-4 py-12">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||||
<div>
|
<div data-anim-footer-item>
|
||||||
<h3 class="text-slate-900 font-semibold text-lg mb-3">{{ $siteName }}</h3>
|
<h3 class="text-slate-900 font-semibold text-lg mb-3">{{ $siteName }}</h3>
|
||||||
<p class="text-sm text-slate-500 leading-relaxed">{{ $siteDescription }}</p>
|
<p class="text-sm text-slate-500 leading-relaxed">{{ $siteDescription }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div data-anim-footer-item>
|
||||||
<h4 class="text-slate-900 font-medium mb-4">Quick Links</h4>
|
<h4 class="text-slate-900 font-medium mb-4">Quick Links</h4>
|
||||||
<ul class="space-y-2 text-sm">
|
<ul class="space-y-2 text-sm">
|
||||||
<li><a href="{{ route('home') }}" class="hover:text-slate-900 transition">Home</a></li>
|
<li><a href="{{ route('home') }}" class="hover:text-slate-900 transition">Home</a></li>
|
||||||
@ -374,14 +379,14 @@
|
|||||||
<li><a href="{{ route('listings.index') }}" class="hover:text-slate-900 transition">All Listings</a></li>
|
<li><a href="{{ route('listings.index') }}" class="hover:text-slate-900 transition">All Listings</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div data-anim-footer-item>
|
||||||
<h4 class="text-slate-900 font-medium mb-4">Account</h4>
|
<h4 class="text-slate-900 font-medium mb-4">Account</h4>
|
||||||
<ul class="space-y-2 text-sm">
|
<ul class="space-y-2 text-sm">
|
||||||
<li><a href="{{ $loginRoute }}" class="hover:text-slate-900 transition">{{ __('messages.login') }}</a></li>
|
<li><a href="{{ $loginRoute }}" class="hover:text-slate-900 transition">{{ __('messages.login') }}</a></li>
|
||||||
<li><a href="{{ $registerRoute }}" class="hover:text-slate-900 transition">{{ __('messages.register') }}</a></li>
|
<li><a href="{{ $registerRoute }}" class="hover:text-slate-900 transition">{{ __('messages.register') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div data-anim-footer-item>
|
||||||
<h4 class="text-slate-900 font-medium mb-4">Links</h4>
|
<h4 class="text-slate-900 font-medium mb-4">Links</h4>
|
||||||
<ul class="space-y-2 text-sm mb-4">
|
<ul class="space-y-2 text-sm mb-4">
|
||||||
@if($linkedinUrl)
|
@if($linkedinUrl)
|
||||||
|
|||||||
@ -1322,17 +1322,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="qc-panel">
|
<div class="qc-panel">
|
||||||
<div class="qc-publish-stack">
|
<form class="qc-publish-stack" wire:submit.prevent="publishListing">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="submit"
|
||||||
class="qc-button"
|
class="qc-button"
|
||||||
wire:click="publishListing"
|
wire:loading.attr="disabled"
|
||||||
@disabled($isPublishing)
|
wire:target="publishListing"
|
||||||
>
|
>
|
||||||
{{ $isPublishing ? 'Publishing...' : 'Publish listing' }}
|
<span wire:loading.remove wire:target="publishListing">Publish listing</span>
|
||||||
|
<span wire:loading wire:target="publishListing">Publishing...</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="qc-button-secondary" wire:click="goToStep(4)">Back</button>
|
<button type="button" class="qc-button-secondary" wire:click="goToStep(4)" wire:loading.attr="disabled" wire:target="publishListing">Back</button>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||