mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
447 lines
24 KiB
PHP
447 lines
24 KiB
PHP
@extends('app::layouts.app')
|
||
@section('content')
|
||
@php
|
||
$menuCategories = $categories->take(8);
|
||
$heroListing = $featuredListings->first() ?? $recentListings->first();
|
||
$heroImage = $heroListing?->getFirstMediaUrl('listing-images');
|
||
$listingCards = $recentListings->take(6);
|
||
$demoEnabled = (bool) config('demo.enabled');
|
||
$prepareDemoRoute = $demoEnabled ? route('demo.prepare') : null;
|
||
$prepareDemoRedirect = url()->full();
|
||
$hasDemoSession = (bool) session('is_demo_session') || filled(session('demo_uuid'));
|
||
$demoLandingMode = $demoEnabled && !auth()->check() && !$hasDemoSession;
|
||
$demoTtlMinutes = (int) config('demo.ttl_minutes', 360);
|
||
$demoTtlHours = intdiv($demoTtlMinutes, 60);
|
||
$demoTtlRemainderMinutes = $demoTtlMinutes % 60;
|
||
$demoTtlLabelParts = [];
|
||
|
||
if ($demoTtlHours > 0) {
|
||
$demoTtlLabelParts[] = $demoTtlHours.' '.\Illuminate\Support\Str::plural('hour', $demoTtlHours);
|
||
}
|
||
|
||
if ($demoTtlRemainderMinutes > 0) {
|
||
$demoTtlLabelParts[] = $demoTtlRemainderMinutes.' '.\Illuminate\Support\Str::plural('minute', $demoTtlRemainderMinutes);
|
||
}
|
||
|
||
$demoTtlLabel = $demoTtlLabelParts !== [] ? implode(' ', $demoTtlLabelParts) : '0 minutes';
|
||
$homeSlides = collect($generalSettings['home_slides'] ?? [])
|
||
->filter(fn ($slide): bool => is_array($slide))
|
||
->map(function (array $slide): array {
|
||
$badge = trim((string) ($slide['badge'] ?? ''));
|
||
$title = trim((string) ($slide['title'] ?? ''));
|
||
$subtitle = trim((string) ($slide['subtitle'] ?? ''));
|
||
$primaryButtonText = trim((string) ($slide['primary_button_text'] ?? ''));
|
||
$secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? ''));
|
||
$imagePath = trim((string) ($slide['image_path'] ?? ''));
|
||
|
||
return [
|
||
'badge' => $badge !== '' ? $badge : 'OpenClassify Marketplace',
|
||
'title' => $title !== '' ? $title : 'Sell faster with a cleaner local marketplace.',
|
||
'subtitle' => $subtitle !== '' ? $subtitle : 'Buy and sell everything in your area',
|
||
'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : 'Browse Listings',
|
||
'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : 'Post Listing',
|
||
'image_url' => \Modules\S3\Support\MediaStorage::url($imagePath, $slide['disk'] ?? null),
|
||
];
|
||
})
|
||
->values();
|
||
|
||
if ($homeSlides->isEmpty()) {
|
||
$homeSlides = collect([
|
||
[
|
||
'badge' => 'OpenClassify Marketplace',
|
||
'title' => 'Sell faster with a cleaner local marketplace.',
|
||
'subtitle' => 'Buy and sell everything in your area',
|
||
'primary_button_text' => 'Browse Listings',
|
||
'secondary_button_text' => 'Post Listing',
|
||
'image_url' => null,
|
||
],
|
||
]);
|
||
}
|
||
|
||
@endphp
|
||
|
||
@if($demoLandingMode && $prepareDemoRoute)
|
||
<div class="min-h-screen flex items-center justify-center px-5 py-10">
|
||
<form method="POST" action="{{ $prepareDemoRoute }}" class="w-full max-w-xl rounded-[32px] border border-slate-200 bg-white p-8 md:p-10 shadow-xl">
|
||
@csrf
|
||
<input type="hidden" name="redirect_to" value="{{ $prepareDemoRedirect }}">
|
||
<h1 class="text-3xl md:text-5xl font-extrabold tracking-tight text-slate-950">Prepare Demo</h1>
|
||
<p class="mt-5 text-base md:text-lg leading-8 text-slate-600">
|
||
Launch a private seeded marketplace for this browser. Listings, favorites, inbox data, and admin access are prepared automatically.
|
||
</p>
|
||
<p class="mt-4 text-base text-slate-500">
|
||
This demo is deleted automatically after {{ $demoTtlLabel }}.
|
||
</p>
|
||
<button type="submit" class="mt-8 inline-flex min-h-16 w-full items-center justify-center rounded-full bg-blue-600 px-8 py-4 text-lg font-semibold text-white shadow-lg transition hover:bg-blue-700">
|
||
Prepare Demo
|
||
</button>
|
||
</form>
|
||
</div>
|
||
@else
|
||
<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" 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 -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 data-home-slider data-home-hero-copy>
|
||
<div class="relative min-h-[250px]">
|
||
@foreach($homeSlides as $index => $slide)
|
||
<div
|
||
data-home-slide
|
||
@class(['transition-opacity duration-300', 'hidden' => $index !== 0])
|
||
aria-hidden="{{ $index === 0 ? 'false' : 'true' }}"
|
||
>
|
||
<p class="text-sm uppercase tracking-[0.22em] text-blue-200 font-semibold mb-4">{{ $slide['badge'] }}</p>
|
||
<h1 class="text-4xl md:text-5xl leading-tight font-extrabold max-w-xl">{{ $slide['title'] }}</h1>
|
||
<p class="mt-4 text-blue-100 text-base md:text-lg max-w-xl">{{ $slide['subtitle'] }}</p>
|
||
<div class="mt-8 flex flex-wrap items-center gap-3">
|
||
<a href="{{ route('listings.index') }}" class="bg-white text-blue-900 px-8 py-3 rounded-full font-semibold hover:bg-blue-50 transition">
|
||
{{ $slide['primary_button_text'] }}
|
||
</a>
|
||
@auth
|
||
<a href="{{ route('panel.listings.create') }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
||
{{ $slide['secondary_button_text'] }}
|
||
</a>
|
||
@else
|
||
<a href="{{ route('login') }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
||
{{ $slide['secondary_button_text'] }}
|
||
</a>
|
||
@endauth
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
|
||
@if($homeSlides->count() > 1)
|
||
<div class="mt-8 flex items-center gap-2">
|
||
<button
|
||
type="button"
|
||
data-home-slide-prev
|
||
class="w-8 h-8 rounded-full border border-white/45 text-white grid place-items-center hover:bg-white/15 transition"
|
||
aria-label="Previous slide"
|
||
>
|
||
<span aria-hidden="true">‹</span>
|
||
</button>
|
||
@foreach($homeSlides as $index => $slide)
|
||
<button
|
||
type="button"
|
||
data-home-slide-dot="{{ $index }}"
|
||
@class([
|
||
'h-2.5 rounded-full transition-all',
|
||
'w-7 bg-white' => $index === 0,
|
||
'w-2.5 bg-white/40 hover:bg-white/60' => $index !== 0,
|
||
])
|
||
aria-label="Slide {{ $index + 1 }}"
|
||
></button>
|
||
@endforeach
|
||
<button
|
||
type="button"
|
||
data-home-slide-next
|
||
class="w-8 h-8 rounded-full border border-white/45 text-white grid place-items-center hover:bg-white/15 transition"
|
||
aria-label="Next slide"
|
||
>
|
||
<span aria-hidden="true">›</span>
|
||
</button>
|
||
</div>
|
||
@else
|
||
<div class="mt-8 flex items-center gap-2">
|
||
<span class="w-7 h-2.5 rounded-full bg-white"></span>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
<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="w-full h-full rounded-[24px] bg-white overflow-hidden">
|
||
<div class="px-3 py-2 border-b border-slate-100">
|
||
<p class="text-rose-500 text-sm font-bold">OpenClassify</p>
|
||
<p class="text-[10px] text-slate-400 mt-1">Search listings, categories, and sellers</p>
|
||
</div>
|
||
<div class="p-2 space-y-2">
|
||
<div class="h-10 rounded-xl bg-slate-100"></div>
|
||
<div class="grid grid-cols-3 gap-2">
|
||
<div class="h-9 rounded-lg bg-blue-100"></div>
|
||
<div class="h-9 rounded-lg bg-emerald-100"></div>
|
||
<div class="h-9 rounded-lg bg-amber-100"></div>
|
||
</div>
|
||
<div class="space-y-2">
|
||
<div class="h-14 rounded-xl bg-slate-100"></div>
|
||
<div class="h-14 rounded-xl bg-slate-100"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="absolute right-0 bottom-0 w-[78%] h-[88%] rounded-[28px] bg-gradient-to-br from-white/20 to-blue-500/40 border border-white/20 shadow-2xl flex items-end justify-center p-4 overflow-hidden">
|
||
@foreach($homeSlides as $index => $slide)
|
||
<div
|
||
data-home-slide-visual
|
||
@class(['absolute inset-4 transition-opacity duration-300', 'hidden' => $index !== 0])
|
||
aria-hidden="{{ $index === 0 ? 'false' : 'true' }}"
|
||
>
|
||
@if($slide['image_url'])
|
||
<img src="{{ $slide['image_url'] }}" alt="{{ $slide['title'] }}" class="w-full h-full object-cover rounded-2xl">
|
||
@elseif($heroImage)
|
||
<img src="{{ $heroImage }}" alt="{{ $heroListing?->title }}" class="w-full h-full object-cover rounded-2xl">
|
||
@else
|
||
<div class="w-full h-full rounded-2xl bg-white/90 text-slate-800 flex flex-col justify-center items-center gap-3">
|
||
<span class="text-6xl">◌</span>
|
||
<p class="text-sm font-semibold px-4 text-center">Upload a slide image to make this area feel complete.</p>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section data-home-section>
|
||
<div class="flex items-center justify-between mb-3">
|
||
<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">
|
||
View all
|
||
</a>
|
||
</div>
|
||
<div class="relative">
|
||
<button
|
||
type="button"
|
||
data-trend-prev
|
||
class="hidden lg:inline-flex absolute left-0 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10 w-11 h-11 rounded-full border border-slate-300 bg-white text-slate-700 items-center justify-center shadow-sm hover:bg-slate-50 transition"
|
||
aria-label="Previous trending category"
|
||
>
|
||
<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="m15 18-6-6 6-6"/>
|
||
</svg>
|
||
</button>
|
||
<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)
|
||
@php
|
||
$categoryIconUrl = $category->iconUrl();
|
||
$fallbackLabel = strtoupper(\Illuminate\Support\Str::substr($category->name, 0, 1));
|
||
@endphp
|
||
<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="flex items-center justify-center h-[92px] rounded-[20px] bg-[linear-gradient(180deg,#f8fbff_0%,#eef5ff_100%)]">
|
||
@if($categoryIconUrl)
|
||
<img src="{{ $categoryIconUrl }}" alt="{{ $category->name }}" class="h-14 w-14 object-contain">
|
||
@else
|
||
<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>
|
||
@endif
|
||
</div>
|
||
<div class="pt-4">
|
||
<p class="text-[13px] sm:text-[14px] font-semibold text-slate-900 leading-tight">{{ $category->name }}</p>
|
||
</div>
|
||
</a>
|
||
@endforeach
|
||
</div>
|
||
<button
|
||
type="button"
|
||
data-trend-next
|
||
class="hidden lg:inline-flex absolute right-0 top-1/2 translate-x-1/2 -translate-y-1/2 z-10 w-11 h-11 rounded-full border border-slate-300 bg-white text-slate-700 items-center justify-center shadow-sm hover:bg-slate-50 transition"
|
||
aria-label="Next trending category"
|
||
>
|
||
<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="m9 18 6-6-6-6"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section data-home-section>
|
||
<div class="flex items-center justify-between mb-4">
|
||
<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">
|
||
<span class="w-8 h-8 rounded-full border border-slate-300 grid place-items-center">‹</span>
|
||
<span class="w-8 h-8 rounded-full border border-slate-300 grid place-items-center">›</span>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||
@forelse($listingCards as $listing)
|
||
@php
|
||
$listingImage = $listing->getFirstMediaUrl('listing-images');
|
||
$priceLabel = $listing->price ? number_format((float) $listing->price, 0).' '.$listing->currency : __('messages.free');
|
||
$locationLabel = trim(collect([$listing->city, $listing->country])->filter()->join(', '));
|
||
$isFavorited = in_array($listing->id, $favoriteListingIds ?? [], true);
|
||
@endphp
|
||
<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">
|
||
<a href="{{ route('listings.show', $listing) }}" class="block h-full w-full" aria-label="{{ $listing->title }}">
|
||
@if($listingImage)
|
||
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover">
|
||
@else
|
||
<div class="w-full h-full grid place-items-center text-slate-400">
|
||
<svg class="w-14 h-14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.7" d="M4 16l4.5-4.5a2 2 0 012.8 0L16 16m-1.5-1.5l1.8-1.8a2 2 0 012.8 0L21 14m-7-8h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||
</svg>
|
||
</div>
|
||
@endif
|
||
</a>
|
||
<div class="absolute top-3 left-3 flex items-center gap-2">
|
||
@if($listing->is_featured)
|
||
<span class="bg-amber-300 text-amber-950 text-xs font-bold px-2.5 py-1 rounded-full">Featured</span>
|
||
@endif
|
||
<span class="bg-sky-500 text-white text-xs font-semibold px-2.5 py-1 rounded-full">Spotlight</span>
|
||
</div>
|
||
<div class="absolute top-3 right-3">
|
||
@auth
|
||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
|
||
@csrf
|
||
<button type="submit" class="w-9 h-9 rounded-full grid place-items-center transition {{ $isFavorited ? 'bg-rose-500 text-white' : 'bg-white/90 text-slate-500 hover:text-rose-500' }}">♥</button>
|
||
</form>
|
||
@else
|
||
<a href="{{ route('login') }}" class="w-9 h-9 rounded-full bg-white/90 text-slate-500 hover:text-rose-500 grid place-items-center transition">♡</a>
|
||
@endauth
|
||
</div>
|
||
</div>
|
||
<div class="p-4">
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div>
|
||
<p class="text-3xl font-extrabold tracking-tight text-slate-900">{{ $priceLabel }}</p>
|
||
<h3 class="text-xl font-semibold text-slate-800 mt-1 truncate">{{ $listing->title }}</h3>
|
||
</div>
|
||
<span class="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded-full font-semibold">12 installments</span>
|
||
</div>
|
||
<div class="mt-5 flex items-center justify-between text-sm text-slate-500">
|
||
<span class="truncate">{{ $locationLabel !== '' ? $locationLabel : 'Location not specified' }}</span>
|
||
<span>{{ $listing->created_at->diffForHumans() }}</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
@empty
|
||
<div class="col-span-2 border border-dashed border-slate-300 bg-white rounded-2xl py-20 text-center text-slate-500">
|
||
No listings yet.
|
||
</div>
|
||
@endforelse
|
||
</div>
|
||
</section>
|
||
|
||
<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>
|
||
<h2 class="text-3xl md:text-4xl font-extrabold">{{ __('messages.sell_something') }}</h2>
|
||
<p class="text-slate-300 mt-3">Create a free listing in minutes and reach thousands of buyers.</p>
|
||
</div>
|
||
@auth
|
||
<a href="{{ route('panel.listings.create') }}" class="inline-flex items-center justify-center rounded-full bg-rose-500 hover:bg-rose-600 px-8 py-3 font-semibold transition whitespace-nowrap">
|
||
Post listing
|
||
</a>
|
||
@else
|
||
<a href="{{ route('register') }}" class="inline-flex items-center justify-center rounded-full bg-white text-slate-900 hover:bg-slate-100 px-8 py-3 font-semibold transition whitespace-nowrap">
|
||
Start free
|
||
</a>
|
||
@endauth
|
||
</div>
|
||
</section>
|
||
</div>
|
||
@endif
|
||
<script>
|
||
(() => {
|
||
const setupTrendCategories = () => {
|
||
const track = document.querySelector('[data-trend-track]');
|
||
const previousButton = document.querySelector('[data-trend-prev]');
|
||
const nextButton = document.querySelector('[data-trend-next]');
|
||
|
||
if (!track || !previousButton || !nextButton) {
|
||
return;
|
||
}
|
||
|
||
const scrollAmount = () => Math.max(240, Math.floor(track.clientWidth * 0.7));
|
||
|
||
previousButton.addEventListener('click', () => {
|
||
track.scrollBy({ left: -scrollAmount(), behavior: 'smooth' });
|
||
});
|
||
|
||
nextButton.addEventListener('click', () => {
|
||
track.scrollBy({ left: scrollAmount(), behavior: 'smooth' });
|
||
});
|
||
};
|
||
|
||
const setupHomeSlider = () => {
|
||
const slider = document.querySelector('[data-home-slider]');
|
||
|
||
if (!slider) {
|
||
return;
|
||
}
|
||
|
||
const slides = Array.from(slider.querySelectorAll('[data-home-slide]'));
|
||
const visuals = Array.from(document.querySelectorAll('[data-home-slide-visual]'));
|
||
const dots = Array.from(slider.querySelectorAll('[data-home-slide-dot]'));
|
||
const previousButton = slider.querySelector('[data-home-slide-prev]');
|
||
const nextButton = slider.querySelector('[data-home-slide-next]');
|
||
|
||
if (slides.length <= 1) {
|
||
return;
|
||
}
|
||
|
||
let activeIndex = 0;
|
||
let intervalId = null;
|
||
|
||
const activateSlide = (index) => {
|
||
activeIndex = (index + slides.length) % slides.length;
|
||
|
||
slides.forEach((slide, slideIndex) => {
|
||
const isActive = slideIndex === activeIndex;
|
||
|
||
slide.classList.toggle('hidden', !isActive);
|
||
slide.setAttribute('aria-hidden', isActive ? 'false' : 'true');
|
||
});
|
||
|
||
visuals.forEach((visual, visualIndex) => {
|
||
const isActive = visualIndex === activeIndex;
|
||
|
||
visual.classList.toggle('hidden', !isActive);
|
||
visual.setAttribute('aria-hidden', isActive ? 'false' : 'true');
|
||
});
|
||
|
||
dots.forEach((dot, dotIndex) => {
|
||
const isActive = dotIndex === activeIndex;
|
||
|
||
dot.classList.toggle('w-7', isActive);
|
||
dot.classList.toggle('bg-white', isActive);
|
||
dot.classList.toggle('w-2.5', !isActive);
|
||
dot.classList.toggle('bg-white/40', !isActive);
|
||
});
|
||
};
|
||
|
||
const stopAutoPlay = () => {
|
||
if (intervalId !== null) {
|
||
window.clearInterval(intervalId);
|
||
intervalId = null;
|
||
}
|
||
};
|
||
|
||
const startAutoPlay = () => {
|
||
stopAutoPlay();
|
||
intervalId = window.setInterval(() => activateSlide(activeIndex + 1), 6000);
|
||
};
|
||
|
||
previousButton?.addEventListener('click', () => {
|
||
activateSlide(activeIndex - 1);
|
||
startAutoPlay();
|
||
});
|
||
|
||
nextButton?.addEventListener('click', () => {
|
||
activateSlide(activeIndex + 1);
|
||
startAutoPlay();
|
||
});
|
||
|
||
dots.forEach((dot, index) => {
|
||
dot.addEventListener('click', () => {
|
||
activateSlide(index);
|
||
startAutoPlay();
|
||
});
|
||
});
|
||
|
||
slider.addEventListener('mouseenter', stopAutoPlay);
|
||
slider.addEventListener('mouseleave', startAutoPlay);
|
||
slider.addEventListener('focusin', stopAutoPlay);
|
||
slider.addEventListener('focusout', startAutoPlay);
|
||
|
||
activateSlide(0);
|
||
startAutoPlay();
|
||
};
|
||
|
||
setupHomeSlider();
|
||
setupTrendCategories();
|
||
})();
|
||
</script>
|
||
@endsection
|