Align auth pages and headers

This commit is contained in:
fatihalp 2026-03-07 22:23:53 +03:00
parent dbe1dc97ce
commit 7cd372e183
48 changed files with 3031 additions and 1459 deletions

View File

@ -1,15 +1,5 @@
@extends('app::layouts.app')
@section('content')
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-6">{{ __('messages.categories') }}</h1>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
@foreach($categories as $category)
<a href="{{ route('listings.index', ['category' => $category->id]) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
<div class="text-4xl mb-3">{{ $category->icon ?? '📦' }}</div>
<h3 class="font-semibold text-gray-900">{{ $category->name }}</h3>
<p class="text-gray-500 text-sm mt-1">{{ $category->children->count() }} subcategories</p>
</a>
@endforeach
</div>
</div>
@include('category::partials.index-content', ['categories' => $categories])
@endsection

View File

@ -0,0 +1,118 @@
@php
$categoryCount = $categories->count();
$subcategoryCount = $categories->sum(fn ($category) => $category->children->count());
@endphp
<div class="max-w-[1320px] mx-auto px-4 py-5 md:py-7 space-y-7">
<section class="overflow-hidden rounded-[28px] border border-slate-200/80 bg-white shadow-sm">
<div class="grid gap-8 px-6 py-8 md:px-10 md:py-10 lg:grid-cols-[1.2fr,0.8fr] lg:items-end">
<div class="space-y-5">
<span class="inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.24em] text-blue-700">
Browse categories
</span>
<div class="space-y-3">
<h1 class="max-w-3xl text-3xl font-extrabold tracking-tight text-slate-950 md:text-5xl">
Find the right marketplace section without leaving the same frontend shell.
</h1>
<p class="max-w-2xl text-base leading-7 text-slate-600 md:text-lg">
Explore every top-level category from one clean directory. Header, footer, spacing, and navigation now stay aligned with the rest of the site.
</p>
</div>
<div class="flex flex-wrap gap-3">
<a href="{{ route('listings.index') }}" class="inline-flex min-h-12 items-center justify-center rounded-full bg-blue-600 px-6 text-sm font-semibold text-white shadow-sm transition hover:bg-blue-700">
Browse all listings
</a>
<a href="{{ route('home') }}" class="inline-flex min-h-12 items-center justify-center rounded-full border border-slate-200 bg-white px-6 text-sm font-semibold text-slate-700 transition hover:border-slate-300 hover:text-slate-950">
Go home
</a>
</div>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div class="rounded-[24px] border border-slate-200 bg-slate-50 p-5">
<p class="text-sm font-medium text-slate-500">Root categories</p>
<p class="mt-3 text-3xl font-extrabold tracking-tight text-slate-950">{{ number_format($categoryCount, 0, '.', ',') }}</p>
<p class="mt-2 text-sm text-slate-600">Only top-level sections are shown first for a simpler directory.</p>
</div>
<div class="rounded-[24px] border border-slate-200 bg-slate-50 p-5">
<p class="text-sm font-medium text-slate-500">Subcategories</p>
<p class="mt-3 text-3xl font-extrabold tracking-tight text-slate-950">{{ number_format($subcategoryCount, 0, '.', ',') }}</p>
<p class="mt-2 text-sm text-slate-600">Each card previews its most relevant child sections before you drill in.</p>
</div>
</div>
</div>
</section>
<section class="space-y-4">
<div class="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 class="text-2xl font-extrabold tracking-tight text-slate-950 md:text-3xl">All categories</h2>
<p class="mt-1 text-sm text-slate-500 md:text-base">A single directory view with the same spacing and chrome used across the frontend.</p>
</div>
<p class="text-sm font-medium text-slate-500">{{ number_format($categoryCount, 0, '.', ',') }} categories</p>
</div>
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
@foreach($categories as $category)
@php
$childNames = $category->children
->take(3)
->pluck('name')
->filter()
->implode(' · ');
$extraChildCount = max($category->children->count() - 3, 0);
$icon = match (trim((string) ($category->icon ?? ''))) {
'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));
@endphp
<a
href="{{ route('listings.index', ['category' => $category->id]) }}"
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">
<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">
@if($icon)
<x-dynamic-component :component="$icon" class="h-7 w-7" />
@else
<span class="text-2xl font-semibold">{{ $iconLabel }}</span>
@endif
</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">
{{ number_format($category->children->count(), 0, '.', ',') }} subcategories
</span>
</div>
<div class="mt-6 space-y-3">
<h3 class="text-2xl font-extrabold tracking-tight text-slate-950 transition group-hover:text-blue-700">
{{ $category->name }}
</h3>
<p class="text-sm leading-6 text-slate-600">
{{ $childNames !== '' ? $childNames : 'Open this category to browse available listings and subcategories.' }}
@if($extraChildCount > 0)
<span class="font-semibold text-slate-900">+{{ $extraChildCount }} more</span>
@endif
</p>
</div>
<div class="mt-auto pt-6">
<span class="inline-flex items-center gap-2 text-sm font-semibold text-blue-700">
Explore category
<svg class="h-4 w-4 transition group-hover:translate-x-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 6l6 6-6 6"/>
</svg>
</span>
</div>
</a>
@endforeach
</div>
</section>
</div>

View File

@ -1,15 +1,5 @@
@extends('app::layouts.app')
@section('content')
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-6">{{ __('messages.categories') }}</h1>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
@foreach($categories as $category)
<a href="{{ route('listings.index', ['category' => $category->id]) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
<div class="text-4xl mb-3">{{ $category->icon ?? '📦' }}</div>
<h3 class="font-semibold text-gray-900">{{ $category->name }}</h3>
<p class="text-gray-500 text-sm mt-1">{{ $category->children->count() }} subcategories</p>
</a>
@endforeach
</div>
</div>
@include('category::partials.index-content', ['categories' => $categories])
@endsection

View File

@ -1,15 +1,5 @@
@extends('app::layouts.app')
@section('content')
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-6">{{ __('messages.categories') }}</h1>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
@foreach($categories as $category)
<a href="{{ route('listings.index', ['category' => $category->id]) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
<div class="text-4xl mb-3">{{ $category->icon ?? '📦' }}</div>
<h3 class="font-semibold text-gray-900">{{ $category->name }}</h3>
<p class="text-gray-500 text-sm mt-1">{{ $category->children->count() }} subcategories</p>
</a>
@endforeach
</div>
</div>
@include('category::partials.index-content', ['categories' => $categories])
@endsection

View File

@ -23,6 +23,8 @@ class Listing extends Model implements HasMedia
{
use HasFactory, HasStates, InteractsWithMedia, LogsActivity;
private const DEFAULT_PANEL_EXPIRY_WINDOW_DAYS = 30;
protected $fillable = [
'title', 'description', 'price', 'currency', 'category_id',
'user_id', 'status', 'images', 'custom_fields', 'slug',
@ -240,11 +242,109 @@ class Listing extends Model implements HasMedia
return [
'all' => (int) $counts->sum(),
'active' => (int) ($counts['active'] ?? 0),
'pending' => (int) ($counts['pending'] ?? 0),
'sold' => (int) ($counts['sold'] ?? 0),
'expired' => (int) ($counts['expired'] ?? 0),
];
}
public function panelPrimaryImageUrl(): ?string
{
$url = trim((string) $this->getFirstMediaUrl('listing-images'));
return $url !== '' ? $url : null;
}
public function panelPriceLabel(): string
{
if (is_null($this->price)) {
return 'Ücretsiz';
}
return number_format((float) $this->price, 2, ',', '.').' '.($this->currency ?? 'TL');
}
public function panelStatusMeta(): array
{
return match ($this->statusValue()) {
'sold' => [
'label' => 'Satıldı',
'badge_class' => 'is-success',
'hint' => 'İlan satıldı olarak işaretlendi.',
],
'expired' => [
'label' => 'Süresi doldu',
'badge_class' => 'is-danger',
'hint' => 'Yeniden yayına alınmayı bekliyor.',
],
'pending' => [
'label' => 'İncelemede',
'badge_class' => 'is-warning',
'hint' => 'Moderasyon onayı bekleniyor.',
],
default => [
'label' => 'Yayında',
'badge_class' => 'is-primary',
'hint' => 'Şu anda ziyaretçilere görünüyor.',
],
};
}
public function panelLocationLabel(): string
{
$parts = collect([
trim((string) $this->city),
trim((string) $this->country),
])->filter()->values();
return $parts->isNotEmpty() ? $parts->implode(', ') : 'Konum belirtilmedi';
}
public function panelPublishedAt(): ?Carbon
{
$createdAt = $this->created_at?->copy();
$expiresAt = $this->expires_at?->copy();
if (! $createdAt) {
return $expiresAt;
}
if (! $expiresAt || $expiresAt->greaterThanOrEqualTo($createdAt)) {
return $createdAt;
}
return $expiresAt->subDays(self::DEFAULT_PANEL_EXPIRY_WINDOW_DAYS);
}
public function panelExpirySummary(): string
{
if (! $this->expires_at) {
return 'Süre sınırı yok';
}
$expiresAt = $this->expires_at->copy()->startOfDay();
$days = Carbon::today()->diffInDays($expiresAt, false);
return match (true) {
$days > 0 => $days.' gün kaldı',
$days === 0 => 'Bugün sona eriyor',
default => abs($days).' gün önce sona erdi',
};
}
public function panelVideoSummary(int $total, int $ready, int $pending): ?array
{
if ($total < 1) {
return null;
}
return [
'label' => $total.' video',
'detail' => $ready.' hazır'.($pending > 0 ? ', '.$pending.' işleniyor' : ''),
];
}
public function statusValue(): string
{
return $this->status instanceof ListingStatus

View File

@ -17,7 +17,7 @@
$locationLabel = collect([$listing->city, $listing->country])
->filter(fn ($value) => is_string($value) && trim($value) !== '')
->implode(', ');
->implode(' / ');
$publishedAt = $listing->created_at?->format('M j, Y') ?? 'Recently';
$postedAgo = $listing->created_at?->diffForHumans() ?? 'Listed recently';
@ -37,6 +37,7 @@
? 'Member since '.$listing->user->created_at->format('M Y')
: 'New seller';
$referenceCode = '#'.str_pad((string) $listing->getKey(), 8, '0', STR_PAD_LEFT);
$canContactSeller = $listing->user && (! auth()->check() || (int) auth()->id() !== (int) $listing->user_id);
$isOwnListing = auth()->check() && (int) auth()->id() === (int) $listing->user_id;
@ -52,35 +53,31 @@
$mapQuery = filled($listing->latitude) && filled($listing->longitude)
? trim((string) $listing->latitude).','.trim((string) $listing->longitude)
: $locationLabel;
: str_replace(' / ', ', ', $locationLabel);
$mapUrl = $mapQuery !== ''
? 'https://www.google.com/maps/search/?api=1&query='.urlencode($mapQuery)
: null;
$reportEmail = config('mail.from.address', 'support@example.com');
$reportUrl = 'mailto:'.$reportEmail.'?subject='.rawurlencode('Report listing #'.$listing->getKey());
$reportUrl = 'mailto:'.$reportEmail.'?subject='.rawurlencode('Report listing '.$referenceCode);
$shareUrl = route('listings.show', $listing);
$overviewItems = collect([
['label' => 'Listing ID', 'value' => '#'.$listing->getKey()],
['label' => 'Category', 'value' => $listing->category?->name ?? 'General'],
['label' => 'Location', 'value' => $locationLabel !== '' ? $locationLabel : 'Not specified'],
$specRows = collect([
['label' => 'Price', 'value' => $priceLabel],
['label' => 'Published', 'value' => $publishedAt],
['label' => 'Listing ID', 'value' => $referenceCode],
['label' => 'Category', 'value' => $listing->category?->name ?? 'General'],
['label' => 'Location', 'value' => $locationLabel !== '' ? str_replace(' / ', ' / ', $locationLabel) : 'Not specified'],
])
->filter(fn (array $item) => trim((string) $item['value']) !== '')
->merge(
collect($presentableCustomFields ?? [])->map(fn (array $field) => [
'label' => trim((string) ($field['label'] ?? '')),
'value' => trim((string) ($field['value'] ?? '')),
])
)
->filter(fn (array $item) => $item['label'] !== '' && $item['value'] !== '')
->unique(fn (array $item) => mb_strtolower($item['label']))
->values();
$detailItems = collect($presentableCustomFields ?? [])
->map(fn (array $field) => [
'label' => trim((string) ($field['label'] ?? '')),
'value' => trim((string) ($field['value'] ?? '')),
])
->filter(fn (array $field) => $field['label'] !== '' && $field['value'] !== '')
->values();
if ($detailItems->isEmpty()) {
$detailItems = $overviewItems;
}
@endphp
<div class="lt-wrap">
@ -94,155 +91,227 @@
<span>{{ $displayTitle }}</span>
</nav>
<section class="lt-card lt-hero-card">
<div class="lt-hero-main">
<div class="lt-hero-copy">
<p class="lt-overline">{{ $listing->category?->name ?? 'Marketplace listing' }}</p>
<h1 class="lt-hero-title">{{ $displayTitle }}</h1>
<div class="lt-hero-meta">
<span>{{ $referenceCode }}</span>
<span>{{ $sellerName }}</span>
<span>{{ $postedAgo }}</span>
</div>
</div>
<div class="lt-hero-side">
<div class="lt-hero-price">{{ $priceLabel }}</div>
@if($locationLabel !== '')
<div class="lt-address-chip">{{ $locationLabel }}</div>
@endif
<div class="lt-hero-tools">
<button
type="button"
class="lt-link-action"
data-listing-share
data-share-url="{{ $shareUrl }}"
data-share-title="{{ $displayTitle }}"
>
Share
</button>
@auth
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}" class="lt-inline-form">
@csrf
<button type="submit" class="lt-link-action">
{{ $isListingFavorited ? 'Saved' : 'Save listing' }}
</button>
</form>
@else
<a href="{{ route('login') }}" class="lt-link-action">Save listing</a>
@endauth
</div>
</div>
</div>
</section>
<div class="lt-grid">
<div class="lt-main-column">
<section class="lt-card lt-media-card" data-gallery>
<div class="lt-gallery-main">
<div class="lt-gallery-top">
<div class="lt-gallery-pills">
<span class="lt-badge lt-badge-soft">{{ $listing->category?->name ?? 'Listing' }}</span>
@if($listing->is_featured)
<span class="lt-badge">Featured</span>
@endif
@if($galleryCount > 0)
<span class="lt-badge lt-badge-muted">{{ $galleryCount }} {{ \Illuminate\Support\Str::plural('photo', $galleryCount) }}</span>
@endif
</div>
<div class="lt-media-spec-grid">
<section class="lt-card lt-media-card" data-gallery>
<div class="lt-gallery-main">
<div class="lt-gallery-top">
<div class="lt-gallery-spacer"></div>
<div class="lt-gallery-utility">
<button
type="button"
class="lt-icon-btn"
data-listing-share
data-share-url="{{ $shareUrl }}"
data-share-title="{{ $displayTitle }}"
aria-label="Share listing"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
<path d="M15 8a3 3 0 1 0-2.83-4H12a3 3 0 0 0 .17 1L8.91 6.94a3 3 0 0 0-1.91-.69 3 3 0 1 0 1.91 5.31l3.27 1.94A3 3 0 0 0 12 15a3 3 0 1 0 2.82 4H15a3 3 0 0 0-.17-1l-3.26-1.94a3 3 0 0 0 0-3.12L14.83 10A3 3 0 0 0 15 10h0a3 3 0 0 0 0-2Z"/>
</svg>
</button>
<div class="lt-gallery-utility">
<button
type="button"
class="lt-icon-btn"
data-listing-share
data-share-url="{{ $shareUrl }}"
data-share-title="{{ $displayTitle }}"
aria-label="Share listing"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
<path d="M15 8a3 3 0 1 0-2.83-4H12a3 3 0 0 0 .17 1L8.91 6.94a3 3 0 0 0-1.91-.69 3 3 0 1 0 1.91 5.31l3.27 1.94A3 3 0 0 0 12 15a3 3 0 1 0 2.82 4H15a3 3 0 0 0-.17-1l-3.26-1.94a3 3 0 0 0 0-3.12L14.83 10A3 3 0 0 0 15 10h0a3 3 0 0 0 0-2Z"/>
</svg>
</button>
@auth
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
@csrf
<button
type="submit"
class="lt-icon-btn {{ $isListingFavorited ? 'is-active' : '' }}"
aria-label="{{ $isListingFavorited ? 'Remove from saved listings' : 'Save listing' }}"
>
@auth
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}">
@csrf
<button
type="submit"
class="lt-icon-btn {{ $isListingFavorited ? 'is-active' : '' }}"
aria-label="{{ $isListingFavorited ? 'Remove from saved listings' : 'Save listing' }}"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
<path 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>
</button>
</form>
@else
<a href="{{ route('login') }}" class="lt-icon-btn" aria-label="Sign in to save this listing">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
<path 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>
</button>
</form>
@else
<a href="{{ route('login') }}" class="lt-icon-btn" aria-label="Sign in to save this listing">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
<path 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>
</a>
@endauth
</a>
@endauth
</div>
</div>
@if($initialGalleryImage)
<img src="{{ $initialGalleryImage }}" alt="{{ $displayTitle }}" data-gallery-main>
@else
<div class="lt-gallery-main-empty">No photos uploaded yet.</div>
@endif
@if($galleryCount > 1)
<button type="button" class="lt-gallery-nav" data-gallery-prev aria-label="Previous photo">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m15 18-6-6 6-6"/>
</svg>
</button>
<button type="button" class="lt-gallery-nav" data-gallery-next aria-label="Next photo">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m9 18 6-6-6-6"/>
</svg>
</button>
@endif
@if($galleryCount > 0)
<div class="lt-gallery-count">
<span data-gallery-current>1</span> / <span>{{ $galleryCount }}</span>
</div>
@endif
</div>
@if($initialGalleryImage)
<img src="{{ $initialGalleryImage }}" alt="{{ $displayTitle }}" data-gallery-main>
@else
<div class="lt-gallery-main-empty">No photos uploaded yet.</div>
@if($galleryImages !== [])
<div class="lt-thumbs" data-gallery-thumbs>
@foreach($galleryImages as $index => $image)
<button
type="button"
class="lt-thumb {{ $index === 0 ? 'is-active' : '' }}"
data-gallery-thumb
data-gallery-index="{{ $index }}"
data-gallery-src="{{ $image }}"
aria-label="Open photo {{ $index + 1 }}"
>
<img src="{{ $image }}" alt="{{ $displayTitle }} {{ $index + 1 }}">
</button>
@endforeach
</div>
@endif
</section>
@if($galleryCount > 1)
<button type="button" class="lt-gallery-nav" data-gallery-prev aria-label="Previous photo">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m15 18-6-6 6-6"/>
</svg>
</button>
<button type="button" class="lt-gallery-nav" data-gallery-next aria-label="Next photo">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m9 18 6-6-6-6"/>
</svg>
</button>
@endif
<div class="lt-info-column">
<section class="lt-card lt-spec-card lt-desktop-only">
<div class="lt-card-head">
<div>
<h2 class="lt-section-title">Listing details</h2>
<p class="lt-section-copy">Everything important, laid out cleanly.</p>
</div>
</div>
@if($locationLabel !== '')
<div class="lt-address-chip lt-address-chip-soft">{{ $locationLabel }}</div>
@endif
<div class="lt-spec-table">
@foreach($specRows as $row)
<div class="lt-spec-row">
<span>{{ $row['label'] }}</span>
<strong>{{ $row['value'] }}</strong>
</div>
@endforeach
</div>
<a href="{{ $reportUrl }}" class="lt-inline-link">Report this listing</a>
</section>
<section class="lt-card lt-description-card lt-desktop-only">
<div class="lt-card-head">
<div>
<h2 class="lt-section-title">Description</h2>
<p class="lt-section-copy">Seller notes, condition, and extra context.</p>
</div>
</div>
<div class="lt-description">
{!! nl2br(e($displayDescription)) !!}
</div>
</section>
</div>
</div>
<section class="lt-card lt-mobile-seller-card lt-mobile-only">
<div class="lt-mobile-seller-row">
<div class="lt-avatar">{{ $sellerInitial !== '' ? $sellerInitial : 'S' }}</div>
<div>
<p class="lt-mobile-seller-name">{{ $sellerName }}</p>
<p class="lt-mobile-seller-meta">{{ $sellerMemberText }}</p>
</div>
</div>
</section>
<section class="lt-card lt-mobile-tabs lt-mobile-only" data-detail-tabs>
<div class="lt-tab-list" role="tablist" aria-label="Listing sections">
<button type="button" class="lt-tab-button is-active" data-detail-tab-button data-tab="details" role="tab" aria-selected="true">
Listing details
</button>
<button type="button" class="lt-tab-button" data-detail-tab-button data-tab="description" role="tab" aria-selected="false">
Description
</button>
</div>
@if($galleryImages !== [])
<div class="lt-thumbs" data-gallery-thumbs>
@foreach($galleryImages as $index => $image)
<button
type="button"
class="lt-thumb {{ $index === 0 ? 'is-active' : '' }}"
data-gallery-thumb
data-gallery-index="{{ $index }}"
data-gallery-src="{{ $image }}"
aria-label="Open photo {{ $index + 1 }}"
>
<img src="{{ $image }}" alt="{{ $displayTitle }} {{ $index + 1 }}">
</button>
<div class="lt-tab-panel is-active" data-detail-tab-panel data-panel="details" role="tabpanel">
@if($locationLabel !== '')
<div class="lt-address-chip lt-address-chip-soft">{{ $locationLabel }}</div>
@endif
<div class="lt-spec-table">
@foreach($specRows as $row)
<div class="lt-spec-row">
<span>{{ $row['label'] }}</span>
<strong>{{ $row['value'] }}</strong>
</div>
@endforeach
</div>
@endif
</section>
</div>
<section class="lt-card lt-summary-card">
<div class="lt-summary-copy">
<p class="lt-overline">{{ $listing->category?->name ?? 'Marketplace listing' }}</p>
<div class="lt-price">{{ $priceLabel }}</div>
<h1 class="lt-title">{{ $displayTitle }}</h1>
<div class="lt-summary-meta-row">
<span class="lt-summary-meta-item">{{ $locationLabel !== '' ? $locationLabel : 'Location not specified' }}</span>
<span class="lt-summary-meta-item">{{ $publishedAt }}</span>
<div class="lt-tab-panel" data-detail-tab-panel data-panel="description" role="tabpanel">
<div class="lt-description">
{!! nl2br(e($displayDescription)) !!}
</div>
<p class="lt-subtitle">{{ $postedAgo }}</p>
</div>
<div class="lt-overview-grid">
@foreach($overviewItems as $item)
<div class="lt-overview-item">
<span class="lt-overview-label">{{ $item['label'] }}</span>
<strong class="lt-overview-value">{{ $item['value'] }}</strong>
</div>
@endforeach
</div>
</section>
<section class="lt-card lt-detail-card">
<div class="lt-section-head">
<div>
<h2 class="lt-section-title">Listing details</h2>
<p class="lt-section-copy">A quick view of the important information.</p>
</div>
</div>
<div class="lt-feature-grid">
@foreach($detailItems as $field)
<div class="lt-feature-item">
<span>{{ $field['label'] }}</span>
<strong>{{ $field['value'] }}</strong>
</div>
@endforeach
</div>
</section>
<section class="lt-card lt-detail-card">
<div class="lt-section-head">
<div>
<h2 class="lt-section-title">Description</h2>
<p class="lt-section-copy">Condition notes, usage details, and seller context.</p>
</div>
</div>
<div class="lt-description">
{!! nl2br(e($displayDescription)) !!}
</div>
</section>
@if(($listingVideos ?? collect())->isNotEmpty())
<section class="lt-card lt-detail-card">
<div class="lt-section-head">
<section class="lt-card lt-video-section">
<div class="lt-card-head">
<div>
<h2 class="lt-section-title">Videos</h2>
<p class="lt-section-copy">Extra media attached to this listing.</p>
<p class="lt-section-copy">Additional media attached to the listing.</p>
</div>
</div>
@ -258,8 +327,8 @@
@endif
</div>
<aside class="lt-card lt-side-card">
<div class="lt-seller-panel">
<aside class="lt-side-rail">
<section class="lt-card lt-side-card">
<div class="lt-seller-head">
<div class="lt-avatar">{{ $sellerInitial !== '' ? $sellerInitial : 'S' }}</div>
<div>
@ -269,6 +338,22 @@
</div>
</div>
@if(filled($listing->contact_phone) || filled($listing->contact_email))
<div class="lt-contact-panel">
@if(filled($listing->contact_phone))
<a href="tel:{{ preg_replace('/\s+/', '', (string) $listing->contact_phone) }}" class="lt-contact-primary">
{{ $listing->contact_phone }}
</a>
@endif
@if(filled($listing->contact_email))
<a href="mailto:{{ $listing->contact_email }}" class="lt-contact-secondary">
{{ $listing->contact_email }}
</a>
@endif
</div>
@endif
<div class="lt-actions">
<div class="lt-row-2">
@if(! $listing->user)
@ -341,26 +426,13 @@
@endif
</div>
</div>
</section>
@if(filled($listing->contact_phone) || filled($listing->contact_email))
<div class="lt-contact-strip">
@if(filled($listing->contact_phone))
<a href="tel:{{ preg_replace('/\s+/', '', (string) $listing->contact_phone) }}" class="lt-contact-link">
{{ $listing->contact_phone }}
</a>
@endif
@if(filled($listing->contact_email))
<a href="mailto:{{ $listing->contact_email }}" class="lt-contact-link">
{{ $listing->contact_email }}
</a>
@endif
</div>
@endif
<a href="{{ $reportUrl }}" class="lt-report">Report this listing</a>
<div class="lt-policy">Buyer protection depends on the final agreement with the seller.</div>
</div>
<section class="lt-card lt-safety-card">
<h3 class="lt-safety-title">Safety tips</h3>
<p class="lt-safety-copy">Inspect the item in person, avoid sending money in advance, and confirm the seller identity before closing the deal.</p>
<a href="{{ $reportUrl }}" class="lt-inline-link">Report this listing</a>
</section>
</aside>
</div>
@ -418,8 +490,10 @@
<section class="lt-related">
@if(($relatedListings ?? collect())->isNotEmpty())
<div class="lt-related-head">
<h3 class="lt-related-title">Similar listings</h3>
<p class="lt-related-copy">More listings with a similar feel and category mix.</p>
<div>
<h3 class="lt-related-title">Similar listings</h3>
<p class="lt-related-copy">More listings with a similar category and visual profile.</p>
</div>
</div>
<div class="lt-scroll-wrap" data-theme-scroll>
@ -492,6 +566,7 @@
const thumbButtons = Array.from(galleryRoot.querySelectorAll('[data-gallery-thumb]'));
const prevButton = galleryRoot.querySelector('[data-gallery-prev]');
const nextButton = galleryRoot.querySelector('[data-gallery-next]');
const currentCounter = galleryRoot.querySelector('[data-gallery-current]');
if (!mainImage || thumbButtons.length === 0) {
return;
@ -514,6 +589,10 @@
mainImage.src = src;
}
if (currentCounter) {
currentCounter.textContent = String(activeIndex + 1);
}
thumbButtons.forEach((button, buttonIndex) => {
button.classList.toggle('is-active', buttonIndex === activeIndex);
});
@ -577,6 +656,27 @@
}
});
});
document.querySelectorAll('[data-detail-tabs]').forEach((tabsRoot) => {
const buttons = Array.from(tabsRoot.querySelectorAll('[data-detail-tab-button]'));
const panels = Array.from(tabsRoot.querySelectorAll('[data-detail-tab-panel]'));
const activate = (target) => {
buttons.forEach((button) => {
const active = button.dataset.tab === target;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', active ? 'true' : 'false');
});
panels.forEach((panel) => {
panel.classList.toggle('is-active', panel.dataset.panel === target);
});
};
buttons.forEach((button) => {
button.addEventListener('click', () => activate(button.dataset.tab || 'details'));
});
});
})();
</script>
@endsection

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Controllers\Auth;
namespace Modules\User\App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
@ -9,24 +9,18 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
class ConfirmPasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
return view('user::auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
'password' => $request->string('password')->toString(),
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),

View File

@ -0,0 +1,46 @@
<?php
namespace Modules\User\App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationController extends Controller
{
public function notice(Request $request): RedirectResponse|View
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
return view('user::auth.verify-email');
}
public function send(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
public function verify(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Modules\User\App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
class ForgotPasswordController extends Controller
{
public function create(): View
{
return view('user::auth.forgot-password');
}
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
$status = Password::sendResetLink($request->only('email'));
if ($status === Password::RESET_LINK_SENT) {
return back()->with('status', __($status));
}
return back()
->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Modules\User\App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use Modules\User\App\Http\Requests\LoginRequest;
use Modules\User\App\Support\AuthProviderCatalog;
use Modules\User\App\Support\AuthRedirector;
class LoginController extends Controller
{
public function __construct(
private AuthProviderCatalog $providers,
private AuthRedirector $redirector,
) {
}
public function create(Request $request): View
{
$redirectTo = $this->redirector->rememberQueryTarget($request);
return view('user::auth.login', [
'redirectTo' => $redirectTo,
'socialProviders' => $this->providers->enabled('login', $redirectTo),
]);
}
public function store(LoginRequest $request): RedirectResponse
{
$this->redirector->rememberInputTarget($request);
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}

View File

@ -1,18 +1,14 @@
<?php
namespace App\Http\Controllers\Auth;
namespace Modules\User\App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
@ -21,7 +17,7 @@ class PasswordController extends Controller
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
'password' => $validated['password'],
]);
return back()->with('status', 'password-updated');

View File

@ -0,0 +1,51 @@
<?php
namespace Modules\User\App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use Modules\User\App\Http\Requests\RegisterRequest;
use Modules\User\App\Models\User;
use Modules\User\App\Support\AuthProviderCatalog;
use Modules\User\App\Support\AuthRedirector;
class RegisterController extends Controller
{
public function __construct(
private AuthProviderCatalog $providers,
private AuthRedirector $redirector,
) {
}
public function create(Request $request): View
{
$redirectTo = $this->redirector->rememberQueryTarget($request);
return view('user::auth.register', [
'redirectTo' => $redirectTo,
'socialProviders' => $this->providers->enabled('register', $redirectTo),
]);
}
public function store(RegisterRequest $request): RedirectResponse
{
$this->redirector->rememberInputTarget($request);
$user = User::query()->create([
'name' => $request->fullName(),
'email' => $request->string('email')->toString(),
'password' => $request->string('password')->toString(),
]);
event(new Registered($user));
Auth::guard('web')->login($user);
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Modules\User\App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password as PasswordRule;
use Illuminate\View\View;
use Modules\User\App\Models\User;
class ResetPasswordController extends Controller
{
public function create(Request $request): View
{
return view('user::auth.reset-password', ['request' => $request]);
}
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', PasswordRule::defaults()],
]);
$status = Password::reset(
$request->only('email', 'password', 'token'),
function (User $user) use ($request): void {
$user->forceFill([
'password' => $request->string('password')->toString(),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
},
);
if ($status === Password::PASSWORD_RESET) {
return redirect()->route('login')->with('status', __($status));
}
return back()
->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -1,9 +1,8 @@
<?php
namespace App\Http\Controllers\Auth;
namespace Modules\User\App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Modules\User\App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -11,24 +10,33 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use Modules\User\App\Models\User;
use Modules\User\App\Support\AuthProviderCatalog;
use Modules\User\App\Support\AuthRedirector;
use Throwable;
class SocialAuthController extends Controller
{
private array $allowedProviders = ['google', 'facebook', 'apple'];
public function __construct(
private AuthProviderCatalog $providers,
private AuthRedirector $redirector,
) {
}
public function redirect(string $provider): RedirectResponse
public function redirect(Request $request, string $provider): RedirectResponse
{
abort_unless($this->isProviderAllowed($provider), 404);
abort_unless($this->isProviderEnabled($provider), 404);
abort_unless($this->providers->isAllowed($provider), 404);
abort_unless($this->providers->isEnabled($provider), 404);
$this->redirector->rememberQueryTarget($request);
return $this->driver($provider)->redirect();
}
public function callback(Request $request, string $provider): RedirectResponse
{
abort_unless($this->isProviderAllowed($provider), 404);
abort_unless($this->isProviderEnabled($provider), 404);
abort_unless($this->providers->isAllowed($provider), 404);
abort_unless($this->providers->isEnabled($provider), 404);
try {
$oauthUser = $this->driver($provider)->user();
@ -42,6 +50,16 @@ class SocialAuthController extends Controller
->withErrors(['email' => __('Unable to read social account identity.')]);
}
$user = $this->resolveUser($provider, $oauthUser);
Auth::guard('web')->login($user, true);
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
private function resolveUser(string $provider, mixed $oauthUser): User
{
$socialiteUser = DB::table('socialite_users')
->where('provider', $provider)
->where('provider_id', (string) $oauthUser->getId())
@ -81,13 +99,10 @@ class SocialAuthController extends Controller
],
);
Auth::guard('web')->login($user, true);
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
return $user;
}
private function driver(string $provider)
private function driver(string $provider): mixed
{
$driver = Socialite::driver($provider)
->redirectUrl(route('auth.social.callback', ['provider' => $provider], absolute: true));
@ -98,16 +113,4 @@ class SocialAuthController extends Controller
return $driver;
}
private function isProviderAllowed(string $provider): bool
{
return in_array($provider, $this->allowedProviders, true);
}
private function isProviderEnabled(string $provider): bool
{
return (bool) config("services.{$provider}.enabled", false)
&& filled(config("services.{$provider}.client_id"))
&& filled(config("services.{$provider}.client_secret"));
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace App\Http\Requests\Auth;
namespace Modules\User\App\Http\Requests;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
@ -11,19 +11,11 @@ use Illuminate\Validation\ValidationException;
class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
@ -32,11 +24,6 @@ class LoginRequest extends FormRequest
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
@ -52,11 +39,6 @@ class LoginRequest extends FormRequest
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
@ -75,9 +57,6 @@ class LoginRequest extends FormRequest
]);
}
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string
{
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());

View File

@ -0,0 +1,33 @@
<?php
namespace Modules\User\App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
use Modules\User\App\Models\User;
class RegisterRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'first_name' => ['required', 'string', 'max:120'],
'last_name' => ['required', 'string', 'max:120'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique((new User())->getTable(), 'email')],
'password' => ['required', Password::defaults()],
'terms' => ['accepted'],
'marketing_opt_in' => ['nullable', 'boolean'],
];
}
public function fullName(): string
{
return trim($this->string('first_name')->toString().' '.$this->string('last_name')->toString());
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Modules\User\App\Support;
use Illuminate\Support\Collection;
class AuthProviderCatalog
{
private const DEFINITIONS = [
'google' => [
'label' => 'Google',
'login_label' => 'Sign in with Google',
'register_label' => 'Create account with Google',
],
'apple' => [
'label' => 'Apple',
'login_label' => 'Sign in with Apple',
'register_label' => 'Create account with Apple',
],
'facebook' => [
'label' => 'Facebook',
'login_label' => 'Sign in with Facebook',
'register_label' => 'Create account with Facebook',
],
];
public function enabled(string $context, ?string $redirectTo = null): Collection
{
return collect(self::DEFINITIONS)
->filter(fn (array $provider, string $slug): bool => $this->isEnabled($slug))
->map(function (array $provider, string $slug) use ($context, $redirectTo): array {
return [
'id' => $slug,
'label' => $provider['label'],
'button_label' => $context === 'register'
? $provider['register_label']
: $provider['login_label'],
'url' => route('auth.social.redirect', array_filter([
'provider' => $slug,
'redirect' => $redirectTo,
])),
];
})
->values();
}
public function isAllowed(string $provider): bool
{
return array_key_exists($provider, self::DEFINITIONS);
}
public function isEnabled(string $provider): bool
{
return $this->isAllowed($provider)
&& (bool) config("services.{$provider}.enabled", false)
&& filled(config("services.{$provider}.client_id"))
&& filled(config("services.{$provider}.client_secret"));
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Modules\User\App\Support;
use Illuminate\Http\Request;
class AuthRedirector
{
public function rememberQueryTarget(Request $request, string $key = 'redirect'): ?string
{
return $this->remember($request, $request->query($key));
}
public function rememberInputTarget(Request $request, string $key = 'redirect'): ?string
{
return $this->remember($request, $request->input($key));
}
public function sanitize(?string $target): ?string
{
$target = trim((string) $target);
if ($target === '' || str_starts_with($target, '//')) {
return null;
}
if (str_starts_with($target, '/')) {
return $target;
}
if (! filter_var($target, FILTER_VALIDATE_URL)) {
return null;
}
$applicationUrl = parse_url(url('/'));
$targetUrl = parse_url($target);
if (($applicationUrl['host'] ?? null) !== ($targetUrl['host'] ?? null)) {
return null;
}
$path = $targetUrl['path'] ?? '/';
$query = isset($targetUrl['query']) ? '?'.$targetUrl['query'] : '';
$fragment = isset($targetUrl['fragment']) ? '#'.$targetUrl['fragment'] : '';
return $path.$query.$fragment;
}
private function remember(Request $request, ?string $target): ?string
{
$sanitized = $this->sanitize($target);
if ($sanitized !== null) {
$request->session()->put('url.intended', $sanitized);
}
return $sanitized;
}
}

View File

@ -0,0 +1,45 @@
@extends('user::layouts.auth')
@section('title', 'Confirm password')
@section('content')
<div class="user-auth-copy">
<p class="user-auth-kicker">Security</p>
<h1 class="user-auth-title">Confirm password</h1>
<p class="user-auth-subtitle">This is a secure area. Enter your password once to continue.</p>
</div>
<form method="POST" action="{{ route('password.confirm') }}" class="user-auth-form">
@csrf
<div class="user-auth-field" x-data="{ show: false }">
<label for="password" class="user-auth-label">Password</label>
<div class="user-auth-input-wrap">
<input
id="password"
name="password"
x-bind:type="show ? 'text' : 'password'"
class="user-auth-input has-trailing"
required
autocomplete="current-password"
autofocus
placeholder="Enter your password"
>
<button type="button" class="user-auth-toggle" x-on:click="show = !show" x-bind:aria-label="show ? 'Hide password' : 'Show password'">
<svg x-show="!show" viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/>
<circle cx="12" cy="12" r="3" stroke-width="1.8"/>
</svg>
<svg x-show="show" x-cloak viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m3 3 18 18M10.6 10.6A3 3 0 0 0 14.8 14.8M9.9 5.1A10.4 10.4 0 0 1 12 4.9c6 0 9.5 6 9.5 6a17.6 17.6 0 0 1-2.8 3.5M6.2 6.3C3.8 8 2.5 10.1 2.5 10.1s3.5 6 9.5 6c1.6 0 3.1-.3 4.4-.8"/>
</svg>
</button>
</div>
@error('password')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="user-auth-primary">Confirm password</button>
</form>
@endsection

View File

@ -0,0 +1,43 @@
@extends('user::layouts.auth')
@section('title', 'Reset password')
@section('content')
<div class="user-auth-copy">
<p class="user-auth-kicker">Password</p>
<h1 class="user-auth-title">Reset password</h1>
<p class="user-auth-subtitle">Enter your email and we will send a secure reset link.</p>
</div>
@if (session('status'))
<div class="user-auth-status is-success">{{ session('status') }}</div>
@endif
<form method="POST" action="{{ route('password.email') }}" class="user-auth-form">
@csrf
<div class="user-auth-field">
<label for="email" class="user-auth-label">Email address</label>
<input
id="email"
name="email"
type="email"
value="{{ old('email') }}"
class="user-auth-input"
required
autofocus
placeholder="Enter your email"
>
@error('email')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="user-auth-primary">Send reset link</button>
</form>
<p class="user-auth-switch">
Remembered your password?
<a href="{{ route('login') }}" class="user-auth-link">Back to sign in</a>
</p>
@endsection

View File

@ -0,0 +1,87 @@
@extends('user::layouts.auth')
@section('title', 'Sign in')
@section('content')
<div class="user-auth-copy">
<p class="user-auth-kicker">Account</p>
<h1 class="user-auth-title">Sign in</h1>
</div>
@if (session('status'))
<div class="user-auth-status is-success">{{ session('status') }}</div>
@endif
<form method="POST" action="{{ route('login') }}" class="user-auth-form">
@csrf
@if(filled($redirectTo))
<input type="hidden" name="redirect" value="{{ $redirectTo }}">
@endif
<div class="user-auth-field">
<label for="email" class="user-auth-label">Email address</label>
<input
id="email"
name="email"
type="email"
value="{{ old('email') }}"
class="user-auth-input"
autocomplete="username"
required
autofocus
placeholder="Enter your email"
>
@error('email')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<div class="user-auth-field" x-data="{ show: false }">
<label for="password" class="user-auth-label">Password</label>
<div class="user-auth-input-wrap">
<input
id="password"
name="password"
x-bind:type="show ? 'text' : 'password'"
class="user-auth-input has-trailing"
autocomplete="current-password"
required
placeholder="Enter your password"
>
<button type="button" class="user-auth-toggle" x-on:click="show = !show" x-bind:aria-label="show ? 'Hide password' : 'Show password'">
<svg x-show="!show" viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/>
<circle cx="12" cy="12" r="3" stroke-width="1.8"/>
</svg>
<svg x-show="show" x-cloak viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m3 3 18 18M10.6 10.6A3 3 0 0 0 14.8 14.8M9.9 5.1A10.4 10.4 0 0 1 12 4.9c6 0 9.5 6 9.5 6a17.6 17.6 0 0 1-2.8 3.5M6.2 6.3C3.8 8 2.5 10.1 2.5 10.1s3.5 6 9.5 6c1.6 0 3.1-.3 4.4-.8"/>
</svg>
</button>
</div>
@error('password')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<div class="user-auth-help-row">
<label class="user-auth-checkbox is-inline">
<input id="remember" type="checkbox" name="remember">
<span>Keep me signed in</span>
</label>
<a href="{{ route('password.request') }}" class="user-auth-link">Forgot password?</a>
</div>
<button type="submit" class="user-auth-primary">
Sign in with email
</button>
</form>
<p class="user-auth-switch">
New here?
<a href="{{ route('register', array_filter(['redirect' => $redirectTo])) }}" class="user-auth-link">Create account</a>
</p>
@include('user::auth.partials.social-buttons', ['socialProviders' => $socialProviders])
@endsection

View File

@ -0,0 +1,40 @@
@php
$socialProviders = collect($socialProviders ?? [])->values();
@endphp
@if($socialProviders->isNotEmpty())
<div class="user-auth-divider" aria-hidden="true">
<span>OR</span>
</div>
<div class="user-auth-social-list">
@foreach($socialProviders as $provider)
<a href="{{ $provider['url'] }}" class="user-auth-social-button">
<span class="user-auth-social-icon" aria-hidden="true">
@switch($provider['id'])
@case('google')
<svg viewBox="0 0 24 24" class="h-5 w-5">
<path fill="#EA4335" d="M12 10.2v3.9h5.5c-.2 1.2-.9 2.2-1.8 2.9l3 2.3c1.8-1.6 2.8-4 2.8-6.9 0-.7-.1-1.5-.2-2.2H12Z"/>
<path fill="#34A853" d="M12 21c2.5 0 4.7-.8 6.3-2.2l-3-2.3c-.8.6-2 .9-3.3.9-2.5 0-4.6-1.7-5.4-4l-3.1 2.4A9.5 9.5 0 0 0 12 21Z"/>
<path fill="#4A90E2" d="M6.6 13.4a5.6 5.6 0 0 1 0-2.8L3.5 8.2a9.5 9.5 0 0 0 0 8.5l3.1-2.4Z"/>
<path fill="#FBBC05" d="M12 6.6c1.4 0 2.7.5 3.7 1.4l2.8-2.8A9.5 9.5 0 0 0 3.5 8.2l3.1 2.4c.8-2.3 2.9-4 5.4-4Z"/>
</svg>
@break
@case('apple')
<svg viewBox="0 0 24 24" class="h-5 w-5 fill-current">
<path d="M16.7 12.6c0-2 1.6-3 1.7-3.1-1-1.5-2.6-1.7-3.2-1.7-1.4-.2-2.7.8-3.4.8-.7 0-1.8-.8-3-.8-1.5 0-2.9.9-3.7 2.2-1.6 2.7-.4 6.7 1.1 8.8.7 1 1.6 2.1 2.8 2 .8 0 1.2-.5 2.2-.5s1.4.5 2.3.5c1.2 0 1.9-1 2.6-2 .8-1.2 1.2-2.4 1.2-2.5 0 0-2.3-.9-2.4-3.7Zm-2.3-6.2c.6-.8 1-1.9.9-3-.9 0-2 .6-2.7 1.3-.6.7-1.1 1.8-.9 2.9 1 0 2-.5 2.7-1.2Z"/>
</svg>
@break
@default
<svg viewBox="0 0 24 24" class="h-5 w-5 fill-current text-[#1877F2]">
<path d="M24 12.1C24 5.4 18.6 0 12 0S0 5.4 0 12.1c0 6 4.4 11 10.1 12v-8.4H7.1v-3.6h3V9.3c0-3 1.8-4.7 4.5-4.7 1.3 0 2.7.2 2.7.2v3h-1.5c-1.5 0-2 .9-2 1.9v2.3h3.4l-.5 3.6h-2.9V24C19.6 23.1 24 18.2 24 12.1Z"/>
</svg>
@endswitch
</span>
<span>{{ $provider['button_label'] }}</span>
</a>
@endforeach
</div>
@endif

View File

@ -0,0 +1,121 @@
@extends('user::layouts.auth')
@section('title', 'Create account')
@section('content')
<div class="user-auth-copy">
<p class="user-auth-kicker">Account</p>
<h1 class="user-auth-title">Create account</h1>
<p class="user-auth-subtitle">Open your account once and manage listings, messages, and saved items from one place.</p>
</div>
<form method="POST" action="{{ route('register') }}" class="user-auth-form">
@csrf
@if(filled($redirectTo))
<input type="hidden" name="redirect" value="{{ $redirectTo }}">
@endif
<div class="user-auth-grid">
<div class="user-auth-field">
<label for="first_name" class="user-auth-label">First name</label>
<input
id="first_name"
name="first_name"
type="text"
value="{{ old('first_name') }}"
class="user-auth-input"
autocomplete="given-name"
required
autofocus
placeholder="First name"
>
@error('first_name')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<div class="user-auth-field">
<label for="last_name" class="user-auth-label">Last name</label>
<input
id="last_name"
name="last_name"
type="text"
value="{{ old('last_name') }}"
class="user-auth-input"
autocomplete="family-name"
required
placeholder="Last name"
>
@error('last_name')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
</div>
<div class="user-auth-field">
<label for="email" class="user-auth-label">Email address</label>
<input
id="email"
name="email"
type="email"
value="{{ old('email') }}"
class="user-auth-input"
autocomplete="username"
required
placeholder="Enter your email"
>
@error('email')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<div class="user-auth-field" x-data="{ show: false }">
<label for="password" class="user-auth-label">Password</label>
<div class="user-auth-input-wrap">
<input
id="password"
name="password"
x-bind:type="show ? 'text' : 'password'"
class="user-auth-input has-trailing"
autocomplete="new-password"
required
placeholder="Create a password"
>
<button type="button" class="user-auth-toggle" x-on:click="show = !show" x-bind:aria-label="show ? 'Hide password' : 'Show password'">
<svg x-show="!show" viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/>
<circle cx="12" cy="12" r="3" stroke-width="1.8"/>
</svg>
<svg x-show="show" x-cloak viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m3 3 18 18M10.6 10.6A3 3 0 0 0 14.8 14.8M9.9 5.1A10.4 10.4 0 0 1 12 4.9c6 0 9.5 6 9.5 6a17.6 17.6 0 0 1-2.8 3.5M6.2 6.3C3.8 8 2.5 10.1 2.5 10.1s3.5 6 9.5 6c1.6 0 3.1-.3 4.4-.8"/>
</svg>
</button>
</div>
@error('password')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<div class="user-auth-checkbox-list">
<label class="user-auth-checkbox">
<input type="checkbox" name="terms" value="1" @checked(old('terms')) required>
<span>Accept terms</span>
</label>
@error('terms')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="user-auth-primary">
Create account
</button>
</form>
<p class="user-auth-switch">
Already have an account?
<a href="{{ route('login', array_filter(['redirect' => $redirectTo])) }}" class="user-auth-link">Sign in</a>
</p>
@include('user::auth.partials.social-buttons', ['socialProviders' => $socialProviders])
@endsection

View File

@ -0,0 +1,64 @@
@extends('user::layouts.auth')
@section('title', 'Choose a new password')
@section('content')
<div class="user-auth-copy">
<p class="user-auth-kicker">Password</p>
<h1 class="user-auth-title">Choose a new password</h1>
<p class="user-auth-subtitle">Set a fresh password for your account and continue to your dashboard.</p>
</div>
<form method="POST" action="{{ route('password.store') }}" class="user-auth-form">
@csrf
<input type="hidden" name="token" value="{{ $request->route('token') }}">
<div class="user-auth-field">
<label for="email" class="user-auth-label">Email address</label>
<input
id="email"
name="email"
type="email"
value="{{ old('email', $request->email) }}"
class="user-auth-input"
required
autofocus
autocomplete="username"
placeholder="Enter your email"
>
@error('email')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<div class="user-auth-field" x-data="{ show: false }">
<label for="password" class="user-auth-label">New password</label>
<div class="user-auth-input-wrap">
<input
id="password"
name="password"
x-bind:type="show ? 'text' : 'password'"
class="user-auth-input has-trailing"
required
autocomplete="new-password"
placeholder="Choose a new password"
>
<button type="button" class="user-auth-toggle" x-on:click="show = !show" x-bind:aria-label="show ? 'Hide password' : 'Show password'">
<svg x-show="!show" viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/>
<circle cx="12" cy="12" r="3" stroke-width="1.8"/>
</svg>
<svg x-show="show" x-cloak viewBox="0 0 24 24" class="h-5 w-5" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m3 3 18 18M10.6 10.6A3 3 0 0 0 14.8 14.8M9.9 5.1A10.4 10.4 0 0 1 12 4.9c6 0 9.5 6 9.5 6a17.6 17.6 0 0 1-2.8 3.5M6.2 6.3C3.8 8 2.5 10.1 2.5 10.1s3.5 6 9.5 6c1.6 0 3.1-.3 4.4-.8"/>
</svg>
</button>
</div>
@error('password')
<p class="user-auth-error">{{ $message }}</p>
@enderror
</div>
<button type="submit" class="user-auth-primary">Save new password</button>
</form>
@endsection

View File

@ -0,0 +1,27 @@
@extends('user::layouts.auth')
@section('title', 'Verify email')
@section('content')
<div class="user-auth-copy">
<p class="user-auth-kicker">Verification</p>
<h1 class="user-auth-title">Verify your email</h1>
<p class="user-auth-subtitle">Before you continue, confirm your email address from the link we sent you.</p>
</div>
@if (session('status') === 'verification-link-sent')
<div class="user-auth-status is-success">A fresh verification link has been sent to your inbox.</div>
@endif
<div class="user-auth-actions-stack">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<button type="submit" class="user-auth-primary">Resend verification email</button>
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="user-auth-secondary">Log out</button>
</form>
</div>
@endsection

View File

@ -0,0 +1,35 @@
@php
$siteName = $generalSettings['site_name'] ?? config('app.name', 'OpenClassify');
$siteLogoUrl = $generalSettings['site_logo_url'] ?? null;
$pageTitle = trim($__env->yieldContent('title'));
@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $pageTitle !== '' ? $pageTitle.' - ' : '' }}{{ $siteName }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="user-auth-page">
<main class="user-auth-shell">
<div class="user-auth-frame">
<section class="user-auth-panel">
<a href="{{ route('home') }}" class="user-auth-brand" aria-label="{{ $siteName }}">
@if($siteLogoUrl)
<img src="{{ $siteLogoUrl }}" alt="{{ $siteName }}" class="user-auth-brand-image">
@else
<span class="brand-logo" aria-hidden="true"></span>
@endif
<span class="user-auth-brand-text">{{ $siteName }}</span>
</a>
<div class="user-auth-card">
@yield('content')
</div>
</section>
</div>
</main>
</body>
</html>

View File

@ -1,10 +1,52 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\User\App\Http\Controllers\Auth\ConfirmPasswordController;
use Modules\User\App\Http\Controllers\Auth\EmailVerificationController;
use Modules\User\App\Http\Controllers\Auth\ForgotPasswordController;
use Modules\User\App\Http\Controllers\Auth\LoginController;
use Modules\User\App\Http\Controllers\Auth\PasswordController;
use Modules\User\App\Http\Controllers\Auth\RegisterController;
use Modules\User\App\Http\Controllers\Auth\ResetPasswordController;
use Modules\User\App\Http\Controllers\Auth\SocialAuthController;
use Modules\User\App\Http\Controllers\ProfileController;
Route::middleware('auth')->group(function () {
Route::redirect('/profile', '/panel/my-profile')->name('profile.edit');
Route::patch('/panel/my-profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/panel/my-profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::middleware('web')->group(function () {
Route::middleware('guest')->group(function () {
Route::get('/register', [RegisterController::class, 'create'])->name('register');
Route::post('/register', [RegisterController::class, 'store']);
Route::get('/login', [LoginController::class, 'create'])->name('login');
Route::post('/login', [LoginController::class, 'store']);
Route::get('/forgot-password', [ForgotPasswordController::class, 'create'])->name('password.request');
Route::post('/forgot-password', [ForgotPasswordController::class, 'store'])->name('password.email');
Route::get('/reset-password/{token}', [ResetPasswordController::class, 'create'])->name('password.reset');
Route::post('/reset-password', [ResetPasswordController::class, 'store'])->name('password.store');
Route::prefix('/auth/social')->name('auth.social.')->group(function () {
Route::get('/{provider}', [SocialAuthController::class, 'redirect'])->name('redirect');
Route::get('/{provider}/callback', [SocialAuthController::class, 'callback'])->name('callback');
});
});
Route::middleware('auth')->group(function () {
Route::get('/verify-email', [EmailVerificationController::class, 'notice'])->name('verification.notice');
Route::get('/verify-email/{id}/{hash}', [EmailVerificationController::class, 'verify'])
->middleware(['signed', 'throttle:6,1'])
->name('verification.verify');
Route::post('/email/verification-notification', [EmailVerificationController::class, 'send'])
->middleware('throttle:6,1')
->name('verification.send');
Route::get('/confirm-password', [ConfirmPasswordController::class, 'show'])->name('password.confirm');
Route::post('/confirm-password', [ConfirmPasswordController::class, 'store']);
Route::put('/password', [PasswordController::class, 'update'])->name('password.update');
Route::post('/logout', [LoginController::class, 'destroy'])->name('logout');
Route::redirect('/profile', '/panel/my-profile')->name('profile.edit');
Route::patch('/panel/my-profile', [ProfileController::class, 'update'])->name('profile.update');
Route::delete('/panel/my-profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});
});

View File

@ -1,82 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthenticatedSessionController extends Controller
{
public function create(): View
{
$redirectTo = $this->sanitizeRedirectTarget(request()->query('redirect'));
if ($redirectTo) {
request()->session()->put('url.intended', $redirectTo);
}
return view('auth.login', [
'redirectTo' => $redirectTo,
]);
}
public function store(LoginRequest $request): RedirectResponse
{
$redirectTo = $this->sanitizeRedirectTarget($request->input('redirect'));
if ($redirectTo) {
$request->session()->put('url.intended', $redirectTo);
}
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
private function sanitizeRedirectTarget(?string $target): ?string
{
$target = trim((string) $target);
if ($target === '' || str_starts_with($target, '//')) {
return null;
}
if (str_starts_with($target, '/')) {
return $target;
}
if (! filter_var($target, FILTER_VALIDATE_URL)) {
return null;
}
$applicationUrl = parse_url(url('/'));
$targetUrl = parse_url($target);
if (($applicationUrl['host'] ?? null) !== ($targetUrl['host'] ?? null)) {
return null;
}
$path = $targetUrl['path'] ?? '/';
$query = isset($targetUrl['query']) ? '?' . $targetUrl['query'] : '';
$fragment = isset($targetUrl['fragment']) ? '#' . $targetUrl['fragment'] : '';
return $path . $query . $fragment;
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class EmailVerificationNotificationController extends Controller
{
/**
* Send a new email verification notification.
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false));
}
$request->user()->sendEmailVerificationNotification();
return back()->with('status', 'verification-link-sent');
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class EmailVerificationPromptController extends Controller
{
/**
* Display the email verification prompt.
*/
public function __invoke(Request $request): RedirectResponse|View
{
return $request->user()->hasVerifiedEmail()
? redirect()->intended(route('dashboard', absolute: false))
: view('auth.verify-email');
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Modules\User\App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Modules\User\App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller
{
public function create(): View
{
return view('auth.register');
}
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@ -46,7 +46,7 @@ class PanelController extends Controller
VideoStatus::Processing->value,
]),
])
->when($search !== '', fn ($query) => $query->where('title', 'like', "%{$search}%"))
->searchTerm($search)
->forPanelStatus($status)
->latest('id')
->paginate(10)

View File

@ -1,17 +0,0 @@
<?php
namespace App\View\Components;
use Illuminate\View\Component;
use Illuminate\View\View;
class GuestLayout extends Component
{
/**
* Get the view / contents that represents the component.
*/
public function render(): View
{
return view('layouts.guest');
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div>
<form method="POST" action="{{ route('password.confirm') }}">
@csrf
<!-- Password -->
<div>
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<div class="flex justify-end mt-4">
<x-primary-button>
{{ __('Confirm') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@ -1,25 +0,0 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
</div>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('password.email') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Email Password Reset Link') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@ -1,80 +0,0 @@
<x-guest-layout>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
@php
$socialProviders = collect([
'google' => 'Google',
'facebook' => 'Facebook',
'apple' => 'Apple',
])->filter(
fn ($label, $provider) => (bool) config("services.{$provider}.enabled")
&& filled(config("services.{$provider}.client_id"))
&& filled(config("services.{$provider}.client_secret"))
);
@endphp
<form method="POST" action="{{ route('login') }}">
@csrf
@php
$redirectInput = old('redirect', $redirectTo ?? request('redirect'));
@endphp
@if(filled($redirectInput))
<input type="hidden" name="redirect" value="{{ $redirectInput }}">
@endif
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Remember Me -->
<div class="block mt-4">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
<span class="ms-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
@if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif
<x-primary-button class="ms-3">
{{ __('Log in') }}
</x-primary-button>
</div>
@if($socialProviders->isNotEmpty())
<div class="mt-6 border-t pt-4">
<p class="text-sm text-gray-600 mb-3">{{ __('Or continue with') }}</p>
<div class="grid gap-2">
@foreach($socialProviders as $provider => $label)
<a href="{{ route('auth.social.redirect', ['provider' => $provider]) }}"
class="inline-flex items-center justify-center rounded-md border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">
{{ $label }}
</a>
@endforeach
</div>
</div>
@endif
</form>
</x-guest-layout>

View File

@ -1,52 +0,0 @@
<x-guest-layout>
<form method="POST" action="{{ route('register') }}">
@csrf
<!-- Name -->
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>
<x-primary-button class="ms-4">
{{ __('Register') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@ -1,23 +0,0 @@
@extends('app::layouts.app')
@section('title', 'Registration Disabled')
@section('content')
<div class="container mx-auto px-4 py-16">
<div class="max-w-2xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 p-8 text-center">
<h1 class="text-2xl font-bold text-gray-900">Registration is currently disabled</h1>
<p class="mt-3 text-gray-600">
Registration is available only when at least one social login provider is enabled by the admin.
</p>
<div class="mt-6 flex items-center justify-center gap-3">
<a href="{{ route('home') }}" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50">
Back Home
</a>
<a href="{{ route('login') }}" class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700">
Log in
</a>
</div>
</div>
</div>
@endsection

View File

@ -1,39 +0,0 @@
<x-guest-layout>
<form method="POST" action="{{ route('password.store') }}">
@csrf
<!-- Password Reset Token -->
<input type="hidden" name="token" value="{{ $request->route('token') }}">
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Reset Password') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@ -1,31 +0,0 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
</div>
@if (session('status') == 'verification-link-sent')
<div class="mb-4 font-medium text-sm text-green-600">
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
</div>
@endif
<div class="mt-4 flex items-center justify-between">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<div>
<x-primary-button>
{{ __('Resend Verification Email') }}
</x-primary-button>
</div>
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
{{ __('Log Out') }}
</button>
</form>
</div>
</x-guest-layout>

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>
</body>
</html>

View File

@ -3,163 +3,297 @@
@section('title', 'İlanlarım')
@section('content')
<div class="max-w-[1320px] mx-auto px-4 py-8">
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
@include('panel.partials.sidebar', ['activeMenu' => 'listings'])
@php
$statusTabs = [
[
'key' => 'all',
'label' => 'Tüm İlanlar',
'count' => (int) ($counts['all'] ?? 0),
'description' => 'Hesabındaki tüm ilanlar',
],
[
'key' => 'sold',
'label' => 'Satıldı',
'count' => (int) ($counts['sold'] ?? 0),
'description' => 'Kapanan satışlar',
],
[
'key' => 'expired',
'label' => 'Süresi Doldu',
'count' => (int) ($counts['expired'] ?? 0),
'description' => 'Yeniden yayın bekleyenler',
],
];
$overviewCards = [
[
'label' => 'Toplam İlan',
'value' => (int) ($counts['all'] ?? 0),
'hint' => 'Panelindeki tüm kayıtlar',
],
[
'label' => 'Yayında',
'value' => (int) ($counts['active'] ?? 0),
'hint' => 'Şu anda ziyaretçilere açık',
],
[
'label' => 'Satıldı',
'value' => (int) ($counts['sold'] ?? 0),
'hint' => 'Satışla kapanan ilanlar',
],
[
'label' => 'Süresi Doldu',
'value' => (int) ($counts['expired'] ?? 0),
'hint' => 'Yeniden yayın bekleyen ilanlar',
],
];
$hasFilters = $search !== '' || $status !== 'all';
$pendingCount = (int) ($counts['pending'] ?? 0);
@endphp
<section class="panel-surface">
<div class="panel-toolbar">
<form method="GET" action="{{ route('panel.listings.index') }}" class="panel-search">
<svg class="panel-search-icon 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="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="{{ $search }}" placeholder="İlan başlığına göre ara" class="panel-search-input focus:outline-none focus:ring-2 focus:ring-rose-200">
<input type="hidden" name="status" value="{{ $status }}">
</form>
<div class="listings-dashboard-page mx-auto max-w-[1320px] px-4 py-6 md:py-8">
<div class="grid gap-6 xl:grid-cols-[300px,minmax(0,1fr)]">
<aside class="listings-dashboard-sidebar space-y-6">
@include('panel.partials.sidebar', ['activeMenu' => 'listings'])
<div class="panel-filter-tabs">
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'all']) }}" class="panel-filter-tab {{ $status === 'all' ? 'is-active' : '' }}">
Tüm İlanlar ({{ $counts['all'] }})
</a>
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'sold']) }}" class="panel-filter-tab {{ $status === 'sold' ? 'is-active' : '' }}">
Satıldı ({{ $counts['sold'] }})
</a>
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'expired']) }}" class="panel-filter-tab {{ $status === 'expired' ? 'is-active' : '' }}">
Süresi Dolmuş ({{ $counts['expired'] }})
</a>
<div class="overflow-hidden rounded-[30px] border border-slate-200/80 bg-white/90 p-6 shadow-[0_20px_55px_rgba(15,23,42,0.08)] backdrop-blur">
<p class="account-section-kicker">Kontrol Merkezi</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">İlanlarını tek bakışta yönet</h2>
<p class="mt-3 text-sm leading-6 text-slate-500">
Yayındaki, satılan ve süresi dolan ilanlarını daha hızlı filtrele. Arama alanı sadece gerekli yerde, aksiyonlar ise her kartta doğrudan görünür.
</p>
<div class="mt-5 rounded-[24px] bg-slate-950 px-5 py-4 text-white shadow-[0_18px_38px_rgba(15,23,42,0.18)]">
<p class="text-[0.68rem] font-semibold uppercase tracking-[0.26em] text-slate-300">Bugün</p>
<p class="mt-2 text-sm leading-6 text-slate-200">
@if ($pendingCount > 0)
{{ $pendingCount }} ilan moderasyon incelemesinde. Onaylanınca burada yayında olarak görünecek.
@else
Bu ekranda ilanlarının durumu, etkileşimi ve yayın süresi birlikte özetlenir.
@endif
</p>
</div>
</div>
</aside>
<section class="space-y-6">
<div class="listings-dashboard-hero">
<div class="min-w-0">
<p class="account-section-kicker">Panel</p>
<h1 class="mt-2 text-[2.3rem] font-semibold leading-tight tracking-[-0.04em] text-slate-950">İlanlarım</h1>
<p class="mt-3 max-w-3xl text-sm leading-6 text-slate-500">
Tüm ilanlarını tek ekranda takip et. Tarih, durum ve etkileşim bilgileri artık daha net; arama ve filtre alanı ise daha kompakt.
</p>
</div>
<div class="flex shrink-0 flex-col gap-3 sm:flex-row sm:items-center">
@if ($hasFilters)
<a href="{{ route('panel.listings.index') }}" class="account-secondary-button">Filtreleri Temizle</a>
@endif
<a href="{{ route('panel.listings.create') }}" class="account-primary-button">Yeni İlan Ver</a>
</div>
</div>
<div class="space-y-4 panel-list-section">
@forelse($listings as $listing)
@php
$listingImage = $listing->getFirstMediaUrl('listing-images');
$priceLabel = !is_null($listing->price)
? number_format((float) $listing->price, 2, ',', '.').' '.($listing->currency ?? 'TL')
: 'Ücretsiz';
$statusLabel = $listing->statusLabel();
$statusBadgeClass = match ($listing->statusValue()) {
'sold' => 'bg-emerald-100 text-emerald-700',
'expired' => 'bg-rose-100 text-rose-700',
'pending' => 'bg-amber-100 text-amber-700',
default => 'bg-blue-100 text-blue-700',
};
$favoriteCount = (int) ($listing->favorited_by_users_count ?? 0);
$viewCount = (int) ($listing->view_count ?? 0);
$expiresAt = $listing->expires_at?->format('d/m/Y');
$videoCount = (int) ($listing->videos_count ?? 0);
$readyVideoCount = (int) ($listing->ready_videos_count ?? 0);
$pendingVideoCount = (int) ($listing->pending_videos_count ?? 0);
@endphp
<article class="panel-list-card">
<div class="panel-list-card-body">
<div class="panel-list-media bg-slate-200">
<a href="{{ route('listings.show', $listing) }}" class="block w-full h-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">Görsel yok</div>
<div class="grid gap-4 sm:grid-cols-2 2xl:grid-cols-4">
@foreach ($overviewCards as $card)
<div class="listings-dashboard-stat">
<p class="text-sm font-semibold text-slate-500">{{ $card['label'] }}</p>
<p class="mt-3 text-4xl font-semibold tracking-[-0.04em] text-slate-950">{{ number_format($card['value']) }}</p>
<p class="mt-2 text-sm leading-6 text-slate-500">{{ $card['hint'] }}</p>
</div>
@endforeach
</div>
<div class="listings-dashboard-filter-shell">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="account-section-kicker">Filtrele</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">Arama ve durum</h2>
<p class="mt-2 text-sm leading-6 text-slate-500">
{{ number_format($listings->total()) }} sonuç içinde başlığa göre ara veya görünümü hızlıca daralt.
</p>
</div>
<form method="GET" action="{{ route('panel.listings.index') }}" class="listings-dashboard-search">
<svg class="listings-dashboard-search-icon h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<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="{{ $search }}"
placeholder="İlan başlığına göre ara"
class="listings-dashboard-search-input"
>
<input type="hidden" name="status" value="{{ $status }}">
<button type="submit" class="listings-dashboard-search-button">Ara</button>
</form>
</div>
<div class="mt-5 flex flex-wrap gap-3">
@foreach ($statusTabs as $tab)
<a
href="{{ route('panel.listings.index', ['search' => $search, 'status' => $tab['key']]) }}"
@class([
'listings-dashboard-tab',
'is-active' => $status === $tab['key'],
])
>
<span class="listings-dashboard-tab-label">{{ $tab['label'] }}</span>
<span class="listings-dashboard-tab-count">{{ number_format($tab['count']) }}</span>
<span class="listings-dashboard-tab-description">{{ $tab['description'] }}</span>
</a>
@endforeach
</div>
</div>
<div class="space-y-4">
@forelse ($listings as $listing)
@php
$statusMeta = $listing->panelStatusMeta();
$listingImage = $listing->panelPrimaryImageUrl();
$priceLabel = $listing->panelPriceLabel();
$favoriteCount = (int) ($listing->favorited_by_users_count ?? 0);
$viewCount = (int) ($listing->view_count ?? 0);
$publishedAt = $listing->panelPublishedAt();
$publishedLabel = $publishedAt?->format('d.m.Y') ?? '-';
$expiresLabel = $listing->expires_at?->format('d.m.Y') ?? 'Süresiz';
$videoCount = (int) ($listing->videos_count ?? 0);
$readyVideoCount = (int) ($listing->ready_videos_count ?? 0);
$pendingVideoCount = (int) ($listing->pending_videos_count ?? 0);
$videoSummary = $listing->panelVideoSummary($videoCount, $readyVideoCount, $pendingVideoCount);
@endphp
<article class="listings-dashboard-card">
<a href="{{ route('listings.show', $listing) }}" class="listings-dashboard-media" aria-label="{{ $listing->title }}">
@if ($listingImage)
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="h-full w-full object-cover">
@else
<div class="listings-dashboard-placeholder">
<span>Görsel Yok</span>
</div>
@endif
</a>
<div class="min-w-0 space-y-5">
<div class="flex flex-wrap items-center gap-2">
<span class="listings-dashboard-status {{ $statusMeta['badge_class'] }}">{{ $statusMeta['label'] }}</span>
@if ($listing->category)
<span class="listings-dashboard-meta-chip">{{ $listing->category->name }}</span>
@endif
</a>
</div>
<div class="panel-list-main">
<div class="panel-list-summary">
<p class="panel-list-price text-slate-900">{{ $priceLabel }}</p>
<span class="panel-status-badge {{ $statusBadgeClass }}">{{ $statusLabel }}</span>
<span class="listings-dashboard-meta-chip">{{ $listing->panelLocationLabel() }}</span>
</div>
<h2 class="panel-list-title text-slate-800">{{ $listing->title }}</h2>
<div class="panel-list-actions">
<details class="relative">
<summary class="inline-flex cursor-pointer list-none items-center gap-2 rounded-full border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">
İşlemler
<svg class="h-4 w-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 9 6 6 6-6"/>
</svg>
</summary>
<div class="space-y-3">
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div class="min-w-0">
<h3 class="listings-dashboard-card-title">{{ $listing->title }}</h3>
<p class="mt-2 text-sm leading-6 text-slate-500">{{ $statusMeta['hint'] }}</p>
</div>
<div class="absolute left-0 top-full z-10 mt-2 min-w-52 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-xl">
<a href="{{ route('panel.listings.edit', $listing) }}" class="block rounded-xl px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-50">
İlanı Düzenle
</a>
<p class="listings-dashboard-price-mobile">{{ $priceLabel }}</p>
</div>
<form method="POST" action="{{ route('panel.listings.destroy', $listing) }}">
@csrf
<button type="submit" class="block w-full rounded-xl px-3 py-2 text-left text-sm font-medium text-slate-700 transition hover:bg-slate-50">
İlanı Kaldır
</button>
</form>
<div class="grid gap-3 md:grid-cols-3">
<div class="listings-dashboard-info-card">
<span class="listings-dashboard-info-label">Yayına alındı</span>
<strong>{{ $publishedLabel }}</strong>
<span>İlk görünür olduğu kayıt tarihi</span>
</div>
@if($listing->statusValue() !== 'sold')
<form method="POST" action="{{ route('panel.listings.mark-sold', $listing) }}">
@csrf
<button type="submit" class="block w-full rounded-xl px-3 py-2 text-left text-sm font-medium text-slate-700 transition hover:bg-slate-50">
Satıldı İşaretle
</button>
</form>
@endif
<div class="listings-dashboard-info-card">
<span class="listings-dashboard-info-label">{{ $listing->expires_at ? 'Bitiş tarihi' : 'Yayın süresi' }}</span>
<strong>{{ $expiresLabel }}</strong>
<span>{{ $listing->panelExpirySummary() }}</span>
</div>
@if($listing->statusValue() === 'expired')
<div class="listings-dashboard-info-card">
<span class="listings-dashboard-info-label">Etkileşim</span>
<strong>{{ number_format($viewCount) }} görüntülenme</strong>
<span>{{ number_format($favoriteCount) }} favori</span>
</div>
</div>
</div>
@if ($videoSummary)
<div class="flex flex-wrap gap-2">
<span class="listings-dashboard-soft-chip">{{ $videoSummary['label'] }}</span>
<span class="listings-dashboard-soft-chip is-muted">{{ $videoSummary['detail'] }}</span>
</div>
@endif
@if ($listing->statusValue() === 'expired')
<div class="listings-dashboard-alert is-danger">
Bu ilanın süresi doldu. Satıldıysa kapatabilir, devam edecekse yeniden yayına alabilirsin.
</div>
@elseif ($listing->statusValue() === 'pending')
<div class="listings-dashboard-alert is-warning">
İlan şu anda moderasyon kontrolünde. Onaylandığında otomatik olarak yayında görünür.
</div>
@endif
<div class="flex flex-col gap-4 border-t border-slate-200/80 pt-5 xl:flex-row xl:items-center xl:justify-between">
<div class="flex flex-wrap gap-3">
<a href="{{ route('listings.show', $listing) }}" class="account-secondary-button">İlanı Gör</a>
<a href="{{ route('panel.listings.edit', $listing) }}" class="account-primary-button">Düzenle</a>
</div>
<div class="flex flex-wrap gap-3">
@if ($listing->statusValue() === 'expired')
<form method="POST" action="{{ route('panel.listings.republish', $listing) }}">
@csrf
<button type="submit" class="block w-full rounded-xl px-3 py-2 text-left text-sm font-medium text-slate-700 transition hover:bg-slate-50">
<button type="submit" class="listings-dashboard-text-button">
Yeniden Yayınla
</button>
</form>
@endif
</div>
</details>
@elseif ($listing->statusValue() !== 'sold')
<form method="POST" action="{{ route('panel.listings.mark-sold', $listing) }}">
@csrf
<button type="submit" class="listings-dashboard-text-button">
Satıldı İşaretle
</button>
</form>
@endif
<form method="POST" action="{{ route('panel.listings.destroy', $listing) }}">
@csrf
<button type="submit" class="listings-dashboard-text-button is-danger">
İlanı Kaldır
</button>
</form>
</div>
</div>
</div>
<div class="panel-list-aside">
<div class="panel-stats">
<div class="panel-stat-box">
<span>👁</span>
<span>{{ $viewCount }}</span>
</div>
<div class="panel-stat-box">
<span></span>
<span>{{ $favoriteCount }}</span>
</div>
</div>
<p class="panel-list-dates">
Yayın Tarihi & Bitiş Tarihi:
<strong class="text-slate-700">
{{ $listing->created_at?->format('d/m/Y') ?? '-' }} - {{ $expiresAt ?: '-' }}
</strong>
</p>
@if($videoCount > 0)
<p class="panel-list-dates">
Video Durumu:
<strong class="text-slate-700">
{{ $videoCount }} toplam, {{ $readyVideoCount }} hazır, {{ $pendingVideoCount }} işleniyor
</strong>
</p>
@endif
</div>
</div>
@if((string) $listing->status === 'expired')
<div class="panel-inline-note">
<strong>Bu ilanın süresi doldu.</strong> Eğer sattıysan, lütfen satıldı olarak işaretle.
</div>
@endif
</article>
<aside class="listings-dashboard-aside">
<p class="listings-dashboard-price">{{ $priceLabel }}</p>
<p class="mt-3 text-sm leading-6 text-slate-500">{{ $statusMeta['hint'] }}</p>
</aside>
</article>
@empty
<div class="panel-empty-state">
Bu filtreye uygun ilan bulunamadı.
</div>
<div class="listings-dashboard-empty">
<p class="account-section-kicker">Boş durum</p>
<h2 class="mt-2 text-2xl font-semibold tracking-[-0.03em] text-slate-950">Bu filtreye uygun ilan bulunamadı</h2>
<p class="mt-3 max-w-xl text-sm leading-6 text-slate-500">
Arama terimini temizleyebilir, farklı bir durum seçebilir veya yeni ilan oluşturarak bu alanı doldurabilirsin.
</p>
<div class="mt-6 flex flex-col gap-3 sm:flex-row">
@if ($hasFilters)
<a href="{{ route('panel.listings.index') }}" class="account-secondary-button">Filtreleri Temizle</a>
@endif
<a href="{{ route('panel.listings.create') }}" class="account-primary-button">Yeni İlan Ver</a>
</div>
</div>
@endforelse
</div>
@if($listings->hasPages())
<div class="mt-5">
{{ $listings->links() }}
</div>
@if ($listings->hasPages())
<div class="mt-5">
{{ $listings->links() }}
</div>
@endif
</section>
</div>

View File

@ -1,57 +0,0 @@
<?php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
Route::get('register', [RegisteredUserController::class, 'create'])
->name('register');
Route::post('register', [RegisteredUserController::class, 'store']);
Route::get('login', [AuthenticatedSessionController::class, 'create'])
->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']);
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
->name('password.email');
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
->name('password.reset');
Route::post('reset-password', [NewPasswordController::class, 'store'])
->name('password.store');
});
Route::middleware('auth')->group(function () {
Route::get('verify-email', EmailVerificationPromptController::class)
->name('verification.notice');
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
->middleware(['signed', 'throttle:6,1'])
->name('verification.verify');
Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
->middleware('throttle:6,1')
->name('verification.send');
Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
->name('password.confirm');
Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});

View File

@ -28,5 +28,3 @@ Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
Route::delete('/videos/{video}', [PanelController::class, 'destroyVideo'])->name('videos.destroy');
Route::get('/my-profile', [PanelController::class, 'profile'])->name('profile.edit');
});
require __DIR__.'/auth.php';