mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
Update partner listings flow
This commit is contained in:
parent
57fa68fdfe
commit
747e7410f4
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('listings', function (Blueprint $table): void {
|
||||||
|
if (! Schema::hasColumn('listings', 'view_count')) {
|
||||||
|
$table->unsignedInteger('view_count')->default(0)->after('is_featured');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('listings', function (Blueprint $table): void {
|
||||||
|
if (Schema::hasColumn('listings', 'view_count')) {
|
||||||
|
$table->dropColumn('view_count');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ namespace Modules\Listing\Http\Controllers;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Conversation;
|
use App\Models\Conversation;
|
||||||
use App\Models\FavoriteSearch;
|
use App\Models\FavoriteSearch;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
||||||
@ -82,6 +83,14 @@ class ListingController extends Controller
|
|||||||
|
|
||||||
public function show(Listing $listing)
|
public function show(Listing $listing)
|
||||||
{
|
{
|
||||||
|
if (
|
||||||
|
Schema::hasColumn('listings', 'view_count')
|
||||||
|
&& (! auth()->check() || (int) auth()->id() !== (int) $listing->user_id)
|
||||||
|
) {
|
||||||
|
$listing->increment('view_count');
|
||||||
|
$listing->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
$listing->loadMissing('user:id,name,email');
|
$listing->loadMissing('user:id,name,email');
|
||||||
$presentableCustomFields = ListingCustomFieldSchemaBuilder::presentableValues(
|
$presentableCustomFields = ListingCustomFieldSchemaBuilder::presentableValues(
|
||||||
$listing->category_id ? (int) $listing->category_id : null,
|
$listing->category_id ? (int) $listing->category_id : null,
|
||||||
@ -127,20 +136,20 @@ class ListingController extends Controller
|
|||||||
public function create()
|
public function create()
|
||||||
{
|
{
|
||||||
if (! auth()->check()) {
|
if (! auth()->check()) {
|
||||||
return redirect()->route('filament.partner.auth.login');
|
return redirect()->route('login');
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('filament.partner.resources.listings.create', ['tenant' => auth()->id()]);
|
return redirect()->route('panel.listings.create');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store()
|
public function store()
|
||||||
{
|
{
|
||||||
if (! auth()->check()) {
|
if (! auth()->check()) {
|
||||||
return redirect()->route('filament.partner.auth.login');
|
return redirect()->route('login');
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('filament.partner.resources.listings.create', ['tenant' => auth()->id()])
|
->route('panel.listings.create')
|
||||||
->with('success', 'Use the Partner Panel to create listings.');
|
->with('success', 'İlan oluşturma ekranına yönlendirildin.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,13 +22,14 @@ class Listing extends Model implements HasMedia
|
|||||||
'title', 'description', 'price', 'currency', 'category_id',
|
'title', 'description', 'price', 'currency', 'category_id',
|
||||||
'user_id', 'status', 'images', 'custom_fields', 'slug',
|
'user_id', 'status', 'images', 'custom_fields', 'slug',
|
||||||
'contact_phone', 'contact_email', 'is_featured', 'expires_at',
|
'contact_phone', 'contact_email', 'is_featured', 'expires_at',
|
||||||
'city', 'country', 'latitude', 'longitude', 'location',
|
'city', 'country', 'latitude', 'longitude', 'location', 'view_count',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'images' => 'array',
|
'images' => 'array',
|
||||||
'custom_fields' => 'array',
|
'custom_fields' => 'array',
|
||||||
'is_featured' => 'boolean',
|
'is_featured' => 'boolean',
|
||||||
|
'view_count' => 'integer',
|
||||||
'expires_at' => 'datetime',
|
'expires_at' => 'datetime',
|
||||||
'price' => 'decimal:2',
|
'price' => 'decimal:2',
|
||||||
'latitude' => 'decimal:7',
|
'latitude' => 'decimal:7',
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
<a href="{{ route('filament.partner.auth.login') }}" class="w-9 h-9 rounded-full bg-white/95 text-slate-500 hover:text-rose-500 grid place-items-center transition" aria-label="Giriş yap">
|
<a href="{{ route('login') }}" class="w-9 h-9 rounded-full bg-white/95 text-slate-500 hover:text-rose-500 grid place-items-center transition" aria-label="Giriş yap">
|
||||||
♥
|
♥
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endauth
|
||||||
@ -90,7 +90,7 @@
|
|||||||
@auth
|
@auth
|
||||||
@if($listing->user_id && (int) $listing->user_id !== (int) auth()->id())
|
@if($listing->user_id && (int) $listing->user_id !== (int) auth()->id())
|
||||||
@if($conversationId)
|
@if($conversationId)
|
||||||
<a href="{{ route('favorites.index', ['tab' => 'listings', 'conversation' => $conversationId]) }}" class="block text-center border border-rose-300 text-rose-600 py-2 rounded hover:bg-rose-50 transition text-sm font-semibold">
|
<a href="{{ route('panel.inbox.index', ['conversation' => $conversationId]) }}" class="block text-center border border-rose-300 text-rose-600 py-2 rounded hover:bg-rose-50 transition text-sm font-semibold">
|
||||||
Sohbete Git
|
Sohbete Git
|
||||||
</a>
|
</a>
|
||||||
@else
|
@else
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@if($existingConversationId)
|
@if($existingConversationId)
|
||||||
<a href="{{ route('favorites.index', ['tab' => 'listings', 'conversation' => $existingConversationId]) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-rose-100 text-rose-700 hover:bg-rose-200 transition">
|
<a href="{{ route('panel.inbox.index', ['conversation' => $existingConversationId]) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-rose-100 text-rose-700 hover:bg-rose-200 transition">
|
||||||
Sohbete Git
|
Sohbete Git
|
||||||
</a>
|
</a>
|
||||||
@else
|
@else
|
||||||
@ -66,7 +66,7 @@
|
|||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
@else
|
@else
|
||||||
<a href="{{ route('filament.partner.auth.login') }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-slate-100 text-slate-700 hover:bg-slate-200 transition">
|
<a href="{{ route('login') }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold bg-slate-100 text-slate-700 hover:bg-slate-200 transition">
|
||||||
Giriş yap ve favorile
|
Giriş yap ve favorile
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endauth
|
||||||
|
|||||||
@ -7,8 +7,5 @@ class PartnerServiceProvider extends ServiceProvider
|
|||||||
{
|
{
|
||||||
public function boot(): void {}
|
public function boot(): void {}
|
||||||
|
|
||||||
public function register(): void
|
public function register(): void {}
|
||||||
{
|
|
||||||
$this->app->register(PartnerPanelProvider::class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use App\Support\PartnerSocialRegistrationAvailability;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
|
|
||||||
class PartnerAuthGatewayController extends Controller
|
|
||||||
{
|
|
||||||
public function login(): RedirectResponse
|
|
||||||
{
|
|
||||||
return redirect()->route('filament.partner.auth.login');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function register(): RedirectResponse | Response
|
|
||||||
{
|
|
||||||
if (PartnerSocialRegistrationAvailability::isAvailable()) {
|
|
||||||
return redirect()
|
|
||||||
->route('filament.partner.auth.login')
|
|
||||||
->with('success', __('Registration is available via social login providers.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->view('auth.registration-disabled', status: Response::HTTP_FORBIDDEN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -53,8 +53,8 @@ class ConversationController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('favorites.index', array_merge(
|
->route('panel.inbox.index', array_merge(
|
||||||
$this->listingTabFilters($request),
|
$this->inboxFilters($request),
|
||||||
['conversation' => $conversation->getKey()],
|
['conversation' => $conversation->getKey()],
|
||||||
))
|
))
|
||||||
->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.');
|
->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.');
|
||||||
@ -83,28 +83,16 @@ class ConversationController extends Controller
|
|||||||
])->save();
|
])->save();
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('favorites.index', array_merge(
|
->route('panel.inbox.index', array_merge(
|
||||||
$this->listingTabFilters($request),
|
$this->inboxFilters($request),
|
||||||
['conversation' => $conversation->getKey()],
|
['conversation' => $conversation->getKey()],
|
||||||
))
|
))
|
||||||
->with('success', 'Mesaj gönderildi.');
|
->with('success', 'Mesaj gönderildi.');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function listingTabFilters(Request $request): array
|
private function inboxFilters(Request $request): array
|
||||||
{
|
{
|
||||||
$filters = [
|
$filters = [];
|
||||||
'tab' => 'listings',
|
|
||||||
];
|
|
||||||
|
|
||||||
$status = (string) $request->string('status');
|
|
||||||
if (in_array($status, ['all', 'active'], true)) {
|
|
||||||
$filters['status'] = $status;
|
|
||||||
}
|
|
||||||
|
|
||||||
$categoryId = $request->integer('category');
|
|
||||||
if ($categoryId > 0) {
|
|
||||||
$filters['category'] = $categoryId;
|
|
||||||
}
|
|
||||||
|
|
||||||
$messageFilter = (string) $request->string('message_filter');
|
$messageFilter = (string) $request->string('message_filter');
|
||||||
if (in_array($messageFilter, ['all', 'unread', 'important'], true)) {
|
if (in_array($messageFilter, ['all', 'unread', 'important'], true)) {
|
||||||
|
|||||||
179
app/Http/Controllers/PanelController.php
Normal file
179
app/Http/Controllers/PanelController.php
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Conversation;
|
||||||
|
use App\Models\ConversationMessage;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
|
||||||
|
class PanelController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): RedirectResponse
|
||||||
|
{
|
||||||
|
return redirect()->route('panel.listings.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('panel.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function listings(Request $request): View
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$search = trim((string) $request->string('search'));
|
||||||
|
$status = (string) $request->string('status', 'all');
|
||||||
|
|
||||||
|
if (! in_array($status, ['all', 'sold', 'expired'], true)) {
|
||||||
|
$status = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
$listings = $user->listings()
|
||||||
|
->with('category:id,name')
|
||||||
|
->withCount('favoritedByUsers')
|
||||||
|
->when($search !== '', fn ($query) => $query->where('title', 'like', "%{$search}%"))
|
||||||
|
->when($status !== 'all', fn ($query) => $query->where('status', $status))
|
||||||
|
->latest('id')
|
||||||
|
->paginate(10)
|
||||||
|
->withQueryString();
|
||||||
|
|
||||||
|
$statusCounts = $user->listings()
|
||||||
|
->selectRaw('status, COUNT(*) as aggregate')
|
||||||
|
->groupBy('status')
|
||||||
|
->pluck('aggregate', 'status');
|
||||||
|
|
||||||
|
$counts = [
|
||||||
|
'all' => (int) $statusCounts->sum(),
|
||||||
|
'sold' => (int) ($statusCounts['sold'] ?? 0),
|
||||||
|
'expired' => (int) ($statusCounts['expired'] ?? 0),
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('panel.listings', [
|
||||||
|
'listings' => $listings,
|
||||||
|
'status' => $status,
|
||||||
|
'search' => $search,
|
||||||
|
'counts' => $counts,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function inbox(Request $request): View
|
||||||
|
{
|
||||||
|
$userId = (int) $request->user()->getKey();
|
||||||
|
|
||||||
|
$messageFilter = (string) $request->string('message_filter', 'all');
|
||||||
|
if (! in_array($messageFilter, ['all', 'unread', 'important'], true)) {
|
||||||
|
$messageFilter = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
$conversations = Conversation::query()
|
||||||
|
->forUser($userId)
|
||||||
|
->when(
|
||||||
|
in_array($messageFilter, ['unread', 'important'], true),
|
||||||
|
fn ($query) => $query->whereHas('messages', fn ($messageQuery) => $messageQuery
|
||||||
|
->where('sender_id', '!=', $userId)
|
||||||
|
->whereNull('read_at'))
|
||||||
|
)
|
||||||
|
->with([
|
||||||
|
'listing:id,title,price,currency,user_id',
|
||||||
|
'buyer:id,name',
|
||||||
|
'seller:id,name',
|
||||||
|
'lastMessage:id,conversation_id,sender_id,body,created_at',
|
||||||
|
])
|
||||||
|
->withCount([
|
||||||
|
'messages as unread_count' => fn ($query) => $query
|
||||||
|
->where('sender_id', '!=', $userId)
|
||||||
|
->whereNull('read_at'),
|
||||||
|
])
|
||||||
|
->orderByDesc('last_message_at')
|
||||||
|
->orderByDesc('updated_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$selectedConversation = null;
|
||||||
|
$selectedConversationId = $request->integer('conversation');
|
||||||
|
|
||||||
|
if ($selectedConversationId <= 0 && $conversations->isNotEmpty()) {
|
||||||
|
$selectedConversationId = (int) $conversations->first()->getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($selectedConversationId > 0) {
|
||||||
|
$selectedConversation = $conversations->firstWhere('id', $selectedConversationId);
|
||||||
|
|
||||||
|
if ($selectedConversation) {
|
||||||
|
$selectedConversation->load([
|
||||||
|
'listing:id,title,price,currency,user_id',
|
||||||
|
'messages' => fn ($query) => $query
|
||||||
|
->with('sender:id,name')
|
||||||
|
->orderBy('created_at'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ConversationMessage::query()
|
||||||
|
->where('conversation_id', $selectedConversation->getKey())
|
||||||
|
->where('sender_id', '!=', $userId)
|
||||||
|
->whereNull('read_at')
|
||||||
|
->update([
|
||||||
|
'read_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation {
|
||||||
|
if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) {
|
||||||
|
$conversation->unread_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $conversation;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('panel.inbox', [
|
||||||
|
'conversations' => $conversations,
|
||||||
|
'selectedConversation' => $selectedConversation,
|
||||||
|
'messageFilter' => $messageFilter,
|
||||||
|
'quickMessages' => [
|
||||||
|
'Merhaba',
|
||||||
|
'İlan hâlâ satışta mı?',
|
||||||
|
'Son fiyat nedir?',
|
||||||
|
'Teşekkürler',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyListing(Request $request, Listing $listing): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->guardListingOwner($request, $listing);
|
||||||
|
$listing->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'İlan kaldırıldı.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markListingAsSold(Request $request, Listing $listing): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->guardListingOwner($request, $listing);
|
||||||
|
$listing->forceFill([
|
||||||
|
'status' => 'sold',
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
return back()->with('success', 'İlan satıldı olarak işaretlendi.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function republishListing(Request $request, Listing $listing): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->guardListingOwner($request, $listing);
|
||||||
|
$listing->forceFill([
|
||||||
|
'status' => 'active',
|
||||||
|
'expires_at' => now()->addDays(30),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
return back()->with('success', 'İlan yeniden yayına alındı.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function guardListingOwner(Request $request, Listing $listing): void
|
||||||
|
{
|
||||||
|
if ((int) $listing->user_id !== (int) $request->user()->getKey()) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
692
app/Livewire/PanelQuickListingForm.php
Normal file
692
app/Livewire/PanelQuickListingForm.php
Normal file
@ -0,0 +1,692 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Support\QuickListingCategorySuggester;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||||
|
use Livewire\Features\SupportFileUploads\WithFileUploads;
|
||||||
|
use Modules\Category\Models\Category;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
use Modules\Listing\Models\ListingCustomField;
|
||||||
|
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
||||||
|
use Modules\Listing\Support\ListingPanelHelper;
|
||||||
|
use Modules\Location\Models\City;
|
||||||
|
use Modules\Location\Models\Country;
|
||||||
|
use Modules\Profile\Models\Profile;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class PanelQuickListingForm extends Component
|
||||||
|
{
|
||||||
|
use WithFileUploads;
|
||||||
|
|
||||||
|
private const TOTAL_STEPS = 5;
|
||||||
|
|
||||||
|
public array $photos = [];
|
||||||
|
public array $categories = [];
|
||||||
|
public array $countries = [];
|
||||||
|
public array $cities = [];
|
||||||
|
public array $listingCustomFields = [];
|
||||||
|
public array $customFieldValues = [];
|
||||||
|
|
||||||
|
public int $currentStep = 1;
|
||||||
|
public string $categorySearch = '';
|
||||||
|
public ?int $selectedCategoryId = null;
|
||||||
|
public ?int $activeParentCategoryId = null;
|
||||||
|
|
||||||
|
public ?int $detectedCategoryId = null;
|
||||||
|
public ?float $detectedConfidence = null;
|
||||||
|
public ?string $detectedReason = null;
|
||||||
|
public ?string $detectedError = null;
|
||||||
|
public array $detectedAlternatives = [];
|
||||||
|
public bool $isDetecting = false;
|
||||||
|
|
||||||
|
public string $listingTitle = '';
|
||||||
|
public string $price = '';
|
||||||
|
public string $description = '';
|
||||||
|
public ?int $selectedCountryId = null;
|
||||||
|
public ?int $selectedCityId = null;
|
||||||
|
public bool $isPublishing = false;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->loadCategories();
|
||||||
|
$this->loadLocations();
|
||||||
|
$this->hydrateLocationDefaultsFromProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('panel.quick-create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedPhotos(): void
|
||||||
|
{
|
||||||
|
$this->validatePhotos();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedSelectedCountryId(): void
|
||||||
|
{
|
||||||
|
$this->selectedCityId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removePhoto(int $index): void
|
||||||
|
{
|
||||||
|
if (! isset($this->photos[$index])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($this->photos[$index]);
|
||||||
|
$this->photos = array_values($this->photos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function goToStep(int $step): void
|
||||||
|
{
|
||||||
|
$this->currentStep = max(1, min(self::TOTAL_STEPS, $step));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function goToCategoryStep(): void
|
||||||
|
{
|
||||||
|
$this->validatePhotos();
|
||||||
|
$this->currentStep = 2;
|
||||||
|
|
||||||
|
if (! $this->isDetecting && ! $this->detectedCategoryId) {
|
||||||
|
$this->detectCategoryFromImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function goToDetailsStep(): void
|
||||||
|
{
|
||||||
|
$this->validateCategoryStep();
|
||||||
|
$this->currentStep = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function goToFeaturesStep(): void
|
||||||
|
{
|
||||||
|
$this->validateCategoryStep();
|
||||||
|
$this->validateDetailsStep();
|
||||||
|
$this->currentStep = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function goToPreviewStep(): void
|
||||||
|
{
|
||||||
|
$this->validateCategoryStep();
|
||||||
|
$this->validateDetailsStep();
|
||||||
|
$this->validateCustomFieldsStep();
|
||||||
|
$this->currentStep = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detectCategoryFromImage(): void
|
||||||
|
{
|
||||||
|
if ($this->photos === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->isDetecting = true;
|
||||||
|
$this->detectedError = null;
|
||||||
|
$this->detectedReason = null;
|
||||||
|
$this->detectedAlternatives = [];
|
||||||
|
|
||||||
|
$result = app(QuickListingCategorySuggester::class)->suggestFromImage($this->photos[0]);
|
||||||
|
|
||||||
|
$this->isDetecting = false;
|
||||||
|
$this->detectedCategoryId = $result['category_id'];
|
||||||
|
$this->detectedConfidence = $result['confidence'];
|
||||||
|
$this->detectedReason = $result['reason'];
|
||||||
|
$this->detectedError = $result['error'];
|
||||||
|
$this->detectedAlternatives = $result['alternatives'];
|
||||||
|
|
||||||
|
if ($this->detectedCategoryId) {
|
||||||
|
$this->selectCategory($this->detectedCategoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enterCategory(int $categoryId): void
|
||||||
|
{
|
||||||
|
if (! $this->categoryExists($categoryId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->activeParentCategoryId = $categoryId;
|
||||||
|
$this->categorySearch = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function backToRootCategories(): void
|
||||||
|
{
|
||||||
|
$this->activeParentCategoryId = null;
|
||||||
|
$this->categorySearch = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectCategory(int $categoryId): void
|
||||||
|
{
|
||||||
|
if (! $this->categoryExists($categoryId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->selectedCategoryId = $categoryId;
|
||||||
|
$this->loadListingCustomFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function publishListing(): ?RedirectResponse
|
||||||
|
{
|
||||||
|
if ($this->isPublishing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->isPublishing = true;
|
||||||
|
|
||||||
|
$this->validatePhotos();
|
||||||
|
$this->validateCategoryStep();
|
||||||
|
$this->validateDetailsStep();
|
||||||
|
$this->validateCustomFieldsStep();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->createListing();
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
report($exception);
|
||||||
|
$this->isPublishing = false;
|
||||||
|
session()->flash('error', 'İlan oluşturulamadı. Lütfen tekrar deneyin.');
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->isPublishing = false;
|
||||||
|
session()->flash('success', 'İlan başarıyla oluşturuldu.');
|
||||||
|
|
||||||
|
return redirect()->route('panel.listings.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRootCategoriesProperty(): array
|
||||||
|
{
|
||||||
|
return collect($this->categories)
|
||||||
|
->whereNull('parent_id')
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentCategoriesProperty(): array
|
||||||
|
{
|
||||||
|
if (! $this->activeParentCategoryId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = trim((string) $this->categorySearch);
|
||||||
|
$all = collect($this->categories);
|
||||||
|
$parent = $all->firstWhere('id', $this->activeParentCategoryId);
|
||||||
|
$children = $all->where('parent_id', $this->activeParentCategoryId)->values();
|
||||||
|
|
||||||
|
$combined = collect();
|
||||||
|
|
||||||
|
if (is_array($parent)) {
|
||||||
|
$combined->push($parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
$combined = $combined->concat($children);
|
||||||
|
|
||||||
|
return $combined
|
||||||
|
->when(
|
||||||
|
$search !== '',
|
||||||
|
fn (Collection $categories): Collection => $categories->filter(
|
||||||
|
fn (array $category): bool => str_contains(
|
||||||
|
mb_strtolower($category['name']),
|
||||||
|
mb_strtolower($search)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentParentNameProperty(): string
|
||||||
|
{
|
||||||
|
if (! $this->activeParentCategoryId) {
|
||||||
|
return 'Kategori Seçimi';
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = collect($this->categories)->firstWhere('id', $this->activeParentCategoryId);
|
||||||
|
|
||||||
|
return (string) ($category['name'] ?? 'Kategori Seçimi');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentStepTitleProperty(): string
|
||||||
|
{
|
||||||
|
return match ($this->currentStep) {
|
||||||
|
1 => 'Fotoğraf',
|
||||||
|
2 => 'Kategori Seçimi',
|
||||||
|
3 => 'İlan Bilgileri',
|
||||||
|
4 => 'İlan Özellikleri',
|
||||||
|
5 => 'İlan Önizlemesi',
|
||||||
|
default => 'İlan Ver',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSelectedCategoryNameProperty(): ?string
|
||||||
|
{
|
||||||
|
if (! $this->selectedCategoryId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = collect($this->categories)->firstWhere('id', $this->selectedCategoryId);
|
||||||
|
|
||||||
|
return $category['name'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSelectedCategoryPathProperty(): string
|
||||||
|
{
|
||||||
|
if (! $this->selectedCategoryId) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' › ', $this->categoryPathParts($this->selectedCategoryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDetectedAlternativeNamesProperty(): array
|
||||||
|
{
|
||||||
|
if ($this->detectedAlternatives === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$categoriesById = collect($this->categories)->keyBy('id');
|
||||||
|
|
||||||
|
return collect($this->detectedAlternatives)
|
||||||
|
->map(fn (int $id): ?string => $categoriesById[$id]['name'] ?? null)
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableCitiesProperty(): array
|
||||||
|
{
|
||||||
|
if (! $this->selectedCountryId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($this->cities)
|
||||||
|
->where('country_id', $this->selectedCountryId)
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSelectedCountryNameProperty(): ?string
|
||||||
|
{
|
||||||
|
if (! $this->selectedCountryId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$country = collect($this->countries)->firstWhere('id', $this->selectedCountryId);
|
||||||
|
|
||||||
|
return $country['name'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSelectedCityNameProperty(): ?string
|
||||||
|
{
|
||||||
|
if (! $this->selectedCityId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$city = collect($this->cities)->firstWhere('id', $this->selectedCityId);
|
||||||
|
|
||||||
|
return $city['name'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPreviewCustomFieldsProperty(): array
|
||||||
|
{
|
||||||
|
return ListingCustomFieldSchemaBuilder::presentableValues(
|
||||||
|
$this->selectedCategoryId,
|
||||||
|
$this->sanitizedCustomFieldValues(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitleCharactersProperty(): int
|
||||||
|
{
|
||||||
|
return mb_strlen($this->listingTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescriptionCharactersProperty(): int
|
||||||
|
{
|
||||||
|
return mb_strlen($this->description);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentUserNameProperty(): string
|
||||||
|
{
|
||||||
|
return (string) (auth()->user()?->name ?: 'Kullanıcı');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentUserInitialProperty(): string
|
||||||
|
{
|
||||||
|
return Str::upper(Str::substr($this->currentUserName, 0, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function categoryIconComponent(?string $icon): string
|
||||||
|
{
|
||||||
|
return match ($icon) {
|
||||||
|
'car' => 'heroicon-o-truck',
|
||||||
|
'laptop', 'computer' => 'heroicon-o-computer-desktop',
|
||||||
|
'shirt' => 'heroicon-o-swatch',
|
||||||
|
'home', 'sofa' => 'heroicon-o-home-modern',
|
||||||
|
'briefcase' => 'heroicon-o-briefcase',
|
||||||
|
'wrench' => 'heroicon-o-wrench-screwdriver',
|
||||||
|
'football' => 'heroicon-o-trophy',
|
||||||
|
'phone', 'mobile' => 'heroicon-o-device-phone-mobile',
|
||||||
|
default => 'heroicon-o-tag',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validatePhotos(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'photos' => [
|
||||||
|
'required',
|
||||||
|
'array',
|
||||||
|
'min:1',
|
||||||
|
'max:'.config('quick-listing.max_photo_count', 20),
|
||||||
|
],
|
||||||
|
'photos.*' => [
|
||||||
|
'required',
|
||||||
|
'image',
|
||||||
|
'mimes:jpg,jpeg,png',
|
||||||
|
'max:'.config('quick-listing.max_photo_size_kb', 5120),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateCategoryStep(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'selectedCategoryId' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::in(collect($this->categories)->pluck('id')->all()),
|
||||||
|
],
|
||||||
|
], [
|
||||||
|
'selectedCategoryId.required' => 'Lütfen bir kategori seçin.',
|
||||||
|
'selectedCategoryId.in' => 'Geçerli bir kategori seçin.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateDetailsStep(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'listingTitle' => ['required', 'string', 'max:70'],
|
||||||
|
'price' => ['required', 'numeric', 'min:0'],
|
||||||
|
'description' => ['required', 'string', 'max:1450'],
|
||||||
|
'selectedCountryId' => ['required', 'integer', Rule::in(collect($this->countries)->pluck('id')->all())],
|
||||||
|
'selectedCityId' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
function (string $attribute, mixed $value, \Closure $fail): void {
|
||||||
|
$cityExists = collect($this->availableCities)
|
||||||
|
->contains(fn (array $city): bool => $city['id'] === (int) $value);
|
||||||
|
|
||||||
|
if (! $cityExists) {
|
||||||
|
$fail('Seçtiğiniz şehir, seçilen ülkeye ait değil.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
], [
|
||||||
|
'listingTitle.required' => 'İlan başlığı zorunludur.',
|
||||||
|
'listingTitle.max' => 'İlan başlığı en fazla 70 karakter olabilir.',
|
||||||
|
'price.required' => 'Fiyat zorunludur.',
|
||||||
|
'price.numeric' => 'Fiyat sayısal olmalıdır.',
|
||||||
|
'description.required' => 'Açıklama zorunludur.',
|
||||||
|
'description.max' => 'Açıklama en fazla 1450 karakter olabilir.',
|
||||||
|
'selectedCountryId.required' => 'Ülke seçimi zorunludur.',
|
||||||
|
'selectedCityId.required' => 'Şehir seçimi zorunludur.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateCustomFieldsStep(): void
|
||||||
|
{
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
|
foreach ($this->listingCustomFields as $field) {
|
||||||
|
$fieldRules = [];
|
||||||
|
$name = $field['name'];
|
||||||
|
$statePath = "customFieldValues.{$name}";
|
||||||
|
$type = $field['type'];
|
||||||
|
$isRequired = (bool) $field['is_required'];
|
||||||
|
|
||||||
|
if ($type === ListingCustomField::TYPE_BOOLEAN) {
|
||||||
|
$fieldRules[] = 'nullable';
|
||||||
|
$fieldRules[] = 'boolean';
|
||||||
|
} else {
|
||||||
|
$fieldRules[] = $isRequired ? 'required' : 'nullable';
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldRules[] = match ($type) {
|
||||||
|
ListingCustomField::TYPE_TEXT => 'string|max:255',
|
||||||
|
ListingCustomField::TYPE_TEXTAREA => 'string|max:2000',
|
||||||
|
ListingCustomField::TYPE_NUMBER => 'numeric',
|
||||||
|
ListingCustomField::TYPE_DATE => 'date',
|
||||||
|
default => 'sometimes',
|
||||||
|
};
|
||||||
|
|
||||||
|
if ($type === ListingCustomField::TYPE_SELECT) {
|
||||||
|
$options = collect($field['options'] ?? [])->map(fn ($option): string => (string) $option)->all();
|
||||||
|
$fieldRules[] = Rule::in($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rules[$statePath] = $fieldRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rules !== []) {
|
||||||
|
$this->validate($rules);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createListing(): Listing
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profilePhone = Profile::query()
|
||||||
|
->where('user_id', $user->getKey())
|
||||||
|
->value('phone');
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'title' => trim($this->listingTitle),
|
||||||
|
'description' => trim($this->description),
|
||||||
|
'price' => (float) $this->price,
|
||||||
|
'currency' => ListingPanelHelper::defaultCurrency(),
|
||||||
|
'category_id' => $this->selectedCategoryId,
|
||||||
|
'status' => 'pending',
|
||||||
|
'custom_fields' => $this->sanitizedCustomFieldValues(),
|
||||||
|
'contact_email' => (string) $user->email,
|
||||||
|
'contact_phone' => $profilePhone,
|
||||||
|
'country' => $this->selectedCountryName,
|
||||||
|
'city' => $this->selectedCityName,
|
||||||
|
];
|
||||||
|
|
||||||
|
$listing = Listing::createFromFrontend($payload, $user->getKey());
|
||||||
|
|
||||||
|
foreach ($this->photos as $photo) {
|
||||||
|
if (! $photo instanceof TemporaryUploadedFile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$listing
|
||||||
|
->addMedia($photo->getRealPath())
|
||||||
|
->usingFileName($photo->getClientOriginalName())
|
||||||
|
->toMediaCollection('listing-images');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $listing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizedCustomFieldValues(): array
|
||||||
|
{
|
||||||
|
$fieldsByName = collect($this->listingCustomFields)->keyBy('name');
|
||||||
|
|
||||||
|
return collect($this->customFieldValues)
|
||||||
|
->filter(fn ($value, $key): bool => $fieldsByName->has((string) $key))
|
||||||
|
->map(function ($value, $key) use ($fieldsByName): mixed {
|
||||||
|
$field = $fieldsByName->get((string) $key);
|
||||||
|
$type = (string) ($field['type'] ?? ListingCustomField::TYPE_TEXT);
|
||||||
|
|
||||||
|
return match ($type) {
|
||||||
|
ListingCustomField::TYPE_NUMBER => is_numeric($value) ? (float) $value : null,
|
||||||
|
ListingCustomField::TYPE_BOOLEAN => (bool) $value,
|
||||||
|
default => is_string($value) ? trim($value) : $value,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
->filter(function ($value, $key) use ($fieldsByName): bool {
|
||||||
|
$field = $fieldsByName->get((string) $key);
|
||||||
|
$type = (string) ($field['type'] ?? ListingCustomField::TYPE_TEXT);
|
||||||
|
|
||||||
|
if ($type === ListingCustomField::TYPE_BOOLEAN) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! is_null($value) && $value !== '';
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadCategories(): void
|
||||||
|
{
|
||||||
|
$all = Category::query()
|
||||||
|
->where('is_active', true)
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'parent_id', 'icon']);
|
||||||
|
|
||||||
|
$childrenCount = Category::query()
|
||||||
|
->where('is_active', true)
|
||||||
|
->selectRaw('parent_id, count(*) as aggregate')
|
||||||
|
->whereNotNull('parent_id')
|
||||||
|
->groupBy('parent_id')
|
||||||
|
->pluck('aggregate', 'parent_id');
|
||||||
|
|
||||||
|
$this->categories = $all
|
||||||
|
->map(fn (Category $category): array => [
|
||||||
|
'id' => (int) $category->id,
|
||||||
|
'name' => (string) $category->name,
|
||||||
|
'parent_id' => $category->parent_id ? (int) $category->parent_id : null,
|
||||||
|
'icon' => $category->icon,
|
||||||
|
'has_children' => ((int) ($childrenCount[$category->id] ?? 0)) > 0,
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadLocations(): void
|
||||||
|
{
|
||||||
|
$this->countries = Country::query()
|
||||||
|
->where('is_active', true)
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name'])
|
||||||
|
->map(fn (Country $country): array => [
|
||||||
|
'id' => (int) $country->id,
|
||||||
|
'name' => (string) $country->name,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->cities = City::query()
|
||||||
|
->where('is_active', true)
|
||||||
|
->orderBy('name')
|
||||||
|
->get(['id', 'name', 'country_id'])
|
||||||
|
->map(fn (City $city): array => [
|
||||||
|
'id' => (int) $city->id,
|
||||||
|
'name' => (string) $city->name,
|
||||||
|
'country_id' => (int) $city->country_id,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadListingCustomFields(): void
|
||||||
|
{
|
||||||
|
$this->listingCustomFields = ListingCustomField::query()
|
||||||
|
->active()
|
||||||
|
->forCategory($this->selectedCategoryId)
|
||||||
|
->ordered()
|
||||||
|
->get(['name', 'label', 'type', 'is_required', 'placeholder', 'help_text', 'options'])
|
||||||
|
->map(fn (ListingCustomField $field): array => [
|
||||||
|
'name' => (string) $field->name,
|
||||||
|
'label' => (string) $field->label,
|
||||||
|
'type' => (string) $field->type,
|
||||||
|
'is_required' => (bool) $field->is_required,
|
||||||
|
'placeholder' => $field->placeholder,
|
||||||
|
'help_text' => $field->help_text,
|
||||||
|
'options' => collect($field->options ?? [])
|
||||||
|
->map(fn ($option): string => (string) $option)
|
||||||
|
->values()
|
||||||
|
->all(),
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$allowed = collect($this->listingCustomFields)->pluck('name')->all();
|
||||||
|
$this->customFieldValues = collect($this->customFieldValues)->only($allowed)->all();
|
||||||
|
|
||||||
|
foreach ($this->listingCustomFields as $field) {
|
||||||
|
if ($field['type'] === ListingCustomField::TYPE_BOOLEAN && ! array_key_exists($field['name'], $this->customFieldValues)) {
|
||||||
|
$this->customFieldValues[$field['name']] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hydrateLocationDefaultsFromProfile(): void
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = Profile::query()->where('user_id', $user->getKey())->first();
|
||||||
|
|
||||||
|
if (! $profile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$profileCountry = trim((string) ($profile->country ?? ''));
|
||||||
|
$profileCity = trim((string) ($profile->city ?? ''));
|
||||||
|
|
||||||
|
if ($profileCountry !== '') {
|
||||||
|
$country = collect($this->countries)->first(fn (array $country): bool => mb_strtolower($country['name']) === mb_strtolower($profileCountry));
|
||||||
|
|
||||||
|
if (is_array($country)) {
|
||||||
|
$this->selectedCountryId = $country['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($profileCity !== '' && $this->selectedCountryId) {
|
||||||
|
$city = collect($this->availableCities)->first(fn (array $city): bool => mb_strtolower($city['name']) === mb_strtolower($profileCity));
|
||||||
|
|
||||||
|
if (is_array($city)) {
|
||||||
|
$this->selectedCityId = $city['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function categoryExists(int $categoryId): bool
|
||||||
|
{
|
||||||
|
return collect($this->categories)->contains(fn (array $category): bool => $category['id'] === $categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function categoryPathParts(int $categoryId): array
|
||||||
|
{
|
||||||
|
$byId = collect($this->categories)->keyBy('id');
|
||||||
|
$parts = [];
|
||||||
|
$currentId = $categoryId;
|
||||||
|
|
||||||
|
while ($currentId && $byId->has($currentId)) {
|
||||||
|
$category = $byId->get($currentId);
|
||||||
|
|
||||||
|
if (! is_array($category)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts[] = (string) $category['name'];
|
||||||
|
$currentId = $category['parent_id'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_reverse($parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -48,7 +48,6 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata
|
|||||||
{
|
{
|
||||||
return match ($panel->getId()) {
|
return match ($panel->getId()) {
|
||||||
'admin' => $this->hasRole('admin'),
|
'admin' => $this->hasRole('admin'),
|
||||||
'partner' => true,
|
|
||||||
default => false,
|
default => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ use BezhanSalleh\LanguageSwitch\LanguageSwitch;
|
|||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
@ -33,25 +32,8 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::pattern('tenant', '[0-9]+');
|
|
||||||
View::addNamespace('app', resource_path('views'));
|
View::addNamespace('app', resource_path('views'));
|
||||||
|
|
||||||
app()->booted(function (): void {
|
|
||||||
foreach (app('router')->getRoutes() as $route) {
|
|
||||||
$name = $route->getName();
|
|
||||||
|
|
||||||
if (! is_string($name) || ! str_starts_with($name, 'filament.partner.')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! str_contains($route->uri(), '{tenant}')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$route->where('tenant', '[0-9]+');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$fallbackName = config('app.name', 'OpenClassify');
|
$fallbackName = config('app.name', 'OpenClassify');
|
||||||
$fallbackLocale = config('app.locale', 'en');
|
$fallbackLocale = config('app.locale', 'en');
|
||||||
$fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD']));
|
$fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD']));
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Support;
|
|
||||||
|
|
||||||
use App\Settings\GeneralSettings;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
class PartnerSocialRegistrationAvailability
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
private const PROVIDERS = ['google', 'facebook', 'apple'];
|
|
||||||
|
|
||||||
public static function isAvailable(): bool
|
|
||||||
{
|
|
||||||
foreach (self::PROVIDERS as $provider) {
|
|
||||||
if (self::providerEnabled($provider) && self::providerCredentialsReady($provider)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function providerEnabled(string $provider): bool
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
/** @var GeneralSettings $settings */
|
|
||||||
$settings = app(GeneralSettings::class);
|
|
||||||
|
|
||||||
return match ($provider) {
|
|
||||||
'google' => (bool) ($settings->enable_google_login ?? false),
|
|
||||||
'facebook' => (bool) ($settings->enable_facebook_login ?? false),
|
|
||||||
'apple' => (bool) ($settings->enable_apple_login ?? false),
|
|
||||||
default => false,
|
|
||||||
};
|
|
||||||
} catch (Throwable) {
|
|
||||||
return (bool) config("services.{$provider}.enabled", false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function providerCredentialsReady(string $provider): bool
|
|
||||||
{
|
|
||||||
return filled(config("services.{$provider}.client_id"))
|
|
||||||
&& filled(config("services.{$provider}.client_secret"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,5 +3,4 @@
|
|||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
Modules\Admin\Providers\AdminPanelProvider::class,
|
Modules\Admin\Providers\AdminPanelProvider::class,
|
||||||
Modules\Partner\Providers\PartnerPanelProvider::class,
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -4,5 +4,5 @@
|
|||||||
"Location": true,
|
"Location": true,
|
||||||
"Profile": true,
|
"Profile": true,
|
||||||
"Admin": true,
|
"Admin": true,
|
||||||
"Partner": true
|
"Partner": false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,8 @@
|
|||||||
<a href="{{ route('home') }}" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50">
|
<a href="{{ route('home') }}" class="px-4 py-2 rounded-lg border border-gray-300 text-gray-700 hover:bg-gray-50">
|
||||||
Back Home
|
Back Home
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('filament.partner.auth.login') }}" class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700">
|
<a href="{{ route('login') }}" class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700">
|
||||||
Partner Login
|
Giriş Yap
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,14 +18,14 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
@auth
|
@auth
|
||||||
<form method="POST" action="{{ route('filament.partner.auth.logout') }}">
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
<button type="submit" class="px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700">
|
<button type="submit" class="px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700">
|
||||||
Çıkış Yap
|
Çıkış Yap
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
<a href="{{ route('filament.partner.auth.login') }}" class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700">
|
<a href="{{ route('login') }}" class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700">
|
||||||
Giriş Yap
|
Giriş Yap
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endauth
|
||||||
|
|||||||
@ -5,17 +5,7 @@
|
|||||||
@section('content')
|
@section('content')
|
||||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
|
||||||
<aside class="bg-white border border-slate-200">
|
@include('panel.partials.sidebar', ['activeMenu' => 'favorites', 'activeFavoritesTab' => $activeTab])
|
||||||
<a href="{{ route('favorites.index', ['tab' => 'listings']) }}" class="block px-5 py-4 text-base{{ $activeTab === 'listings' ? ' bg-blue-50 text-blue-700 font-semibold' : ' text-slate-700 hover:bg-slate-50' }}">
|
|
||||||
Favori İlanlar
|
|
||||||
</a>
|
|
||||||
<a href="{{ route('favorites.index', ['tab' => 'searches']) }}" class="block px-5 py-4 border-t border-slate-200{{ $activeTab === 'searches' ? ' bg-blue-50 text-blue-700 font-semibold' : ' text-slate-700 hover:bg-slate-50' }}">
|
|
||||||
Favori Aramalar
|
|
||||||
</a>
|
|
||||||
<a href="{{ route('favorites.index', ['tab' => 'sellers']) }}" class="block px-5 py-4 border-t border-slate-200{{ $activeTab === 'sellers' ? ' bg-blue-50 text-blue-700 font-semibold' : ' text-slate-700 hover:bg-slate-50' }}">
|
|
||||||
Favori Satıcılar
|
|
||||||
</a>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section class="bg-white border border-slate-200">
|
<section class="bg-white border border-slate-200">
|
||||||
@if($activeTab === 'listings')
|
@if($activeTab === 'listings')
|
||||||
@ -98,7 +88,7 @@
|
|||||||
<td class="px-4 py-4">
|
<td class="px-4 py-4">
|
||||||
@if($canMessageListing)
|
@if($canMessageListing)
|
||||||
@if($conversationId)
|
@if($conversationId)
|
||||||
<a href="{{ route('favorites.index', array_merge($listingTabQuery, ['conversation' => $conversationId])) }}" class="inline-flex items-center h-10 px-4 border border-rose-300 text-rose-600 text-sm font-semibold rounded-full hover:bg-rose-50 transition">
|
<a href="{{ route('panel.inbox.index', ['conversation' => $conversationId]) }}" class="inline-flex items-center h-10 px-4 border border-rose-300 text-rose-600 text-sm font-semibold rounded-full hover:bg-rose-50 transition">
|
||||||
Sohbete Git
|
Sohbete Git
|
||||||
</a>
|
</a>
|
||||||
@else
|
@else
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<x-filament-panels::page>
|
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||||
<style>
|
<style>
|
||||||
.qc-shell {
|
.qc-shell {
|
||||||
--qc-bg: #ededed;
|
--qc-bg: #ededed;
|
||||||
@ -1257,7 +1257,7 @@
|
|||||||
<span class="qc-avatar">{{ $this->currentUserInitial }}</span>
|
<span class="qc-avatar">{{ $this->currentUserInitial }}</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="qc-seller-name">{{ $this->currentUserName }}</div>
|
<div class="qc-seller-name">{{ $this->currentUserName }}</div>
|
||||||
<div class="qc-seller-email">{{ \Filament\Facades\Filament::auth()->user()?->email }}</div>
|
<div class="qc-seller-email">{{ auth()->user()?->email }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1284,4 +1284,4 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-filament-panels::page>
|
</div>
|
||||||
|
|||||||
@ -35,11 +35,11 @@
|
|||||||
İncele
|
İncele
|
||||||
</a>
|
</a>
|
||||||
@auth
|
@auth
|
||||||
<a href="{{ route('filament.partner.resources.listings.create', ['tenant' => auth()->id()]) }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
<a href="{{ route('panel.listings.create') }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
||||||
{{ __('messages.post_listing') }}
|
{{ __('messages.post_listing') }}
|
||||||
</a>
|
</a>
|
||||||
@else
|
@else
|
||||||
<a href="{{ route('filament.partner.auth.login') }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
<a href="{{ route('login') }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
||||||
{{ __('messages.post_listing') }}
|
{{ __('messages.post_listing') }}
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endauth
|
||||||
@ -145,7 +145,7 @@
|
|||||||
<button type="submit" class="w-9 h-9 rounded-full grid place-items-center transition {{ $isFavorited ? 'bg-rose-500 text-white' : 'bg-white/90 text-slate-500 hover:text-rose-500' }}">♥</button>
|
<button type="submit" class="w-9 h-9 rounded-full grid place-items-center transition {{ $isFavorited ? 'bg-rose-500 text-white' : 'bg-white/90 text-slate-500 hover:text-rose-500' }}">♥</button>
|
||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
<a href="{{ route('filament.partner.auth.login') }}" class="w-9 h-9 rounded-full bg-white/90 text-slate-500 hover:text-rose-500 grid place-items-center transition">♡</a>
|
<a href="{{ route('login') }}" class="w-9 h-9 rounded-full bg-white/90 text-slate-500 hover:text-rose-500 grid place-items-center transition">♡</a>
|
||||||
@endauth
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -187,7 +187,7 @@
|
|||||||
<p class="text-slate-300 mt-3">Dakikalar içinde ücretsiz ilan oluştur, binlerce alıcıya ulaş.</p>
|
<p class="text-slate-300 mt-3">Dakikalar içinde ücretsiz ilan oluştur, binlerce alıcıya ulaş.</p>
|
||||||
</div>
|
</div>
|
||||||
@auth
|
@auth
|
||||||
<a href="{{ route('filament.partner.resources.listings.create', ['tenant' => auth()->id()]) }}" class="inline-flex items-center justify-center rounded-full bg-rose-500 hover:bg-rose-600 px-8 py-3 font-semibold transition whitespace-nowrap">
|
<a href="{{ route('panel.listings.create') }}" class="inline-flex items-center justify-center rounded-full bg-rose-500 hover:bg-rose-600 px-8 py-3 font-semibold transition whitespace-nowrap">
|
||||||
Hemen İlan Ver
|
Hemen İlan Ver
|
||||||
</a>
|
</a>
|
||||||
@else
|
@else
|
||||||
|
|||||||
@ -7,16 +7,13 @@
|
|||||||
$whatsappNumber = $generalSettings['whatsapp'] ?? null;
|
$whatsappNumber = $generalSettings['whatsapp'] ?? null;
|
||||||
$whatsappDigits = preg_replace('/\D+/', '', (string) $whatsappNumber);
|
$whatsappDigits = preg_replace('/\D+/', '', (string) $whatsappNumber);
|
||||||
$whatsappUrl = $whatsappDigits !== '' ? 'https://wa.me/' . $whatsappDigits : null;
|
$whatsappUrl = $whatsappDigits !== '' ? 'https://wa.me/' . $whatsappDigits : null;
|
||||||
$partnerLoginRoute = route('filament.partner.auth.login');
|
$loginRoute = route('login');
|
||||||
$partnerRegisterRoute = route('register');
|
$registerRoute = route('register');
|
||||||
$partnerLogoutRoute = route('filament.partner.auth.logout');
|
$logoutRoute = route('logout');
|
||||||
$partnerCreateRoute = route('partner.listings.create');
|
$panelCreateRoute = auth()->check() ? route('panel.listings.create') : $loginRoute;
|
||||||
$partnerQuickCreateRoute = auth()->check()
|
$panelListingsRoute = auth()->check() ? route('panel.listings.index') : $loginRoute;
|
||||||
? route('filament.partner.resources.listings.quick-create', ['tenant' => auth()->id()])
|
$inboxRoute = auth()->check() ? route('panel.inbox.index') : $loginRoute;
|
||||||
: $partnerLoginRoute;
|
$favoritesRoute = auth()->check() ? route('favorites.index') : $loginRoute;
|
||||||
$partnerDashboardRoute = auth()->check()
|
|
||||||
? route('filament.partner.pages.dashboard', ['tenant' => auth()->id()])
|
|
||||||
: $partnerLoginRoute;
|
|
||||||
$availableLocales = config('app.available_locales', ['en']);
|
$availableLocales = config('app.available_locales', ['en']);
|
||||||
$localeLabels = [
|
$localeLabels = [
|
||||||
'en' => 'English',
|
'en' => 'English',
|
||||||
@ -31,6 +28,7 @@
|
|||||||
'ja' => '日本語',
|
'ja' => '日本語',
|
||||||
];
|
];
|
||||||
$isHomePage = request()->routeIs('home');
|
$isHomePage = request()->routeIs('home');
|
||||||
|
$isSimplePage = trim($__env->yieldContent('simple_page')) === '1';
|
||||||
$homeHeaderCategories = isset($categories) ? collect($categories)->take(8) : collect();
|
$homeHeaderCategories = isset($categories) ? collect($categories)->take(8) : collect();
|
||||||
$locationCountries = collect($headerLocationCountries ?? [])->values();
|
$locationCountries = collect($headerLocationCountries ?? [])->values();
|
||||||
$defaultCountryIso2 = strtoupper((string) config('app.default_country_iso2', 'TR'));
|
$defaultCountryIso2 = strtoupper((string) config('app.default_country_iso2', 'TR'));
|
||||||
@ -132,6 +130,7 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@livewireStyles
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen">
|
<body class="min-h-screen">
|
||||||
<nav class="market-nav-surface sticky top-0 z-50">
|
<nav class="market-nav-surface sticky top-0 z-50">
|
||||||
@ -205,47 +204,35 @@
|
|||||||
|
|
||||||
<div class="ml-auto flex items-center gap-2 md:gap-3">
|
<div class="ml-auto flex items-center gap-2 md:gap-3">
|
||||||
@auth
|
@auth
|
||||||
<a href="{{ route('favorites.index') }}" class="header-utility hidden xl:inline-flex" aria-label="Favoriler">
|
<a href="{{ $favoritesRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Favoriler">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21l-1.45-1.32C5.4 15.03 2 12.01 2 8.31 2 5.3 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.08A6.04 6.04 0 0116.5 3C19.58 3 22 5.3 22 8.31c0 3.7-3.4 6.72-8.55 11.39L12 21z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21l-1.45-1.32C5.4 15.03 2 12.01 2 8.31 2 5.3 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.08A6.04 6.04 0 0116.5 3C19.58 3 22 5.3 22 8.31c0 3.7-3.4 6.72-8.55 11.39L12 21z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ $partnerDashboardRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Panel">
|
<a href="{{ $inboxRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Gelen Kutusu">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6h16a1 1 0 011 1v10a1 1 0 01-1 1H4a1 1 0 01-1-1V7a1 1 0 011-1z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 8l9 6 9-6"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="{{ $panelListingsRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Panel">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 12l9-9 9 9M5 10v10h14V10"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 12l9-9 9 9M5 10v10h14V10"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ $partnerQuickCreateRoute }}" class="hidden md:inline-flex px-4 py-2.5 text-sm font-semibold rounded-full border border-rose-200 text-rose-600 bg-rose-50 hover:bg-rose-100 transition">
|
<a href="{{ $panelCreateRoute }}" class="btn-primary px-4 md:px-5 py-2.5 text-sm font-semibold shadow-sm hover:brightness-95 transition">
|
||||||
Post Fast
|
İlan Ver
|
||||||
</a>
|
</a>
|
||||||
<details class="relative">
|
<form method="POST" action="{{ $logoutRoute }}" class="hidden xl:block">
|
||||||
<summary class="chip-btn list-none cursor-pointer px-3 py-2 text-xs md:text-sm text-slate-700">
|
|
||||||
{{ strtoupper(app()->getLocale()) }}
|
|
||||||
</summary>
|
|
||||||
<div class="absolute right-0 mt-2 bg-white border border-slate-200 shadow-lg rounded-xl overflow-hidden min-w-28">
|
|
||||||
@foreach($availableLocales as $locale)
|
|
||||||
<a href="{{ route('lang.switch', $locale) }}" class="block px-3 py-2 text-sm hover:bg-slate-50 {{ app()->getLocale() === $locale ? 'font-semibold text-rose-500' : 'text-slate-700' }}">
|
|
||||||
{{ $localeLabels[$locale] ?? strtoupper($locale) }}
|
|
||||||
</a>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<a href="{{ $partnerCreateRoute }}" class="btn-primary px-4 md:px-5 py-2.5 text-sm font-semibold shadow-sm hover:brightness-95 transition">
|
|
||||||
Sat
|
|
||||||
</a>
|
|
||||||
<form method="POST" action="{{ $partnerLogoutRoute }}" class="hidden xl:block">
|
|
||||||
@csrf
|
@csrf
|
||||||
<button type="submit" class="text-sm text-slate-500 hover:text-rose-500 transition">{{ __('messages.logout') }}</button>
|
<button type="submit" class="text-sm text-slate-500 hover:text-rose-500 transition">{{ __('messages.logout') }}</button>
|
||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
<a href="{{ $partnerQuickCreateRoute }}" class="hidden md:inline-flex px-4 py-2.5 text-sm font-semibold rounded-full border border-rose-200 text-rose-600 bg-rose-50 hover:bg-rose-100 transition">
|
<a href="{{ $loginRoute }}" class="bg-rose-50 text-rose-500 px-4 md:px-5 py-2.5 rounded-full text-sm font-semibold hover:bg-rose-100 transition">
|
||||||
Post Fast
|
|
||||||
</a>
|
|
||||||
<a href="{{ $partnerLoginRoute }}" class="bg-rose-50 text-rose-500 px-4 md:px-5 py-2.5 rounded-full text-sm font-semibold hover:bg-rose-100 transition">
|
|
||||||
{{ __('messages.login') }}
|
{{ __('messages.login') }}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ $partnerCreateRoute }}" class="btn-primary px-4 md:px-5 py-2.5 text-sm font-semibold shadow-sm hover:brightness-95 transition">
|
<a href="{{ $panelCreateRoute }}" class="btn-primary px-4 md:px-5 py-2.5 text-sm font-semibold shadow-sm hover:brightness-95 transition">
|
||||||
Sat
|
İlan Ver
|
||||||
</a>
|
</a>
|
||||||
@endauth
|
@endauth
|
||||||
</div>
|
</div>
|
||||||
@ -267,11 +254,11 @@
|
|||||||
</form>
|
</form>
|
||||||
<div class="flex items-center gap-2 overflow-x-auto pb-1">
|
<div class="flex items-center gap-2 overflow-x-auto pb-1">
|
||||||
<span class="chip-btn whitespace-nowrap px-4 py-2 text-sm text-slate-700" data-location-label-mobile>Konum seç</span>
|
<span class="chip-btn whitespace-nowrap px-4 py-2 text-sm text-slate-700" data-location-label-mobile>Konum seç</span>
|
||||||
<a href="{{ $partnerQuickCreateRoute }}" class="chip-btn whitespace-nowrap px-4 py-2 text-sm text-rose-600 font-semibold">Post Fast</a>
|
<a href="{{ $panelCreateRoute }}" class="chip-btn whitespace-nowrap px-4 py-2 text-sm text-rose-600 font-semibold">İlan Ver</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($isHomePage && $homeHeaderCategories->isNotEmpty())
|
@if(!$isSimplePage && $isHomePage && $homeHeaderCategories->isNotEmpty())
|
||||||
<div class="mt-4 border-t border-slate-200 pt-3 overflow-x-auto">
|
<div class="mt-4 border-t border-slate-200 pt-3 overflow-x-auto">
|
||||||
<div class="flex items-center gap-2 min-w-max pb-1">
|
<div class="flex items-center gap-2 min-w-max pb-1">
|
||||||
<a href="{{ route('categories.index') }}" class="chip-btn inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-100 transition">
|
<a href="{{ route('categories.index') }}" class="chip-btn inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-100 transition">
|
||||||
@ -287,7 +274,7 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@elseif(! $isHomePage)
|
@elseif(! $isSimplePage && ! $isHomePage)
|
||||||
<div class="mt-3 flex items-center gap-2 text-sm overflow-x-auto pb-1">
|
<div class="mt-3 flex items-center gap-2 text-sm overflow-x-auto pb-1">
|
||||||
<a href="{{ route('home') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.home') }}</a>
|
<a href="{{ route('home') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.home') }}</a>
|
||||||
<a href="{{ route('categories.index') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.categories') }}</a>
|
<a href="{{ route('categories.index') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.categories') }}</a>
|
||||||
@ -325,8 +312,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="text-white font-medium mb-4">Hesap</h4>
|
<h4 class="text-white font-medium mb-4">Hesap</h4>
|
||||||
<ul class="space-y-2 text-sm">
|
<ul class="space-y-2 text-sm">
|
||||||
<li><a href="{{ $partnerLoginRoute }}" class="hover:text-white transition">{{ __('messages.login') }}</a></li>
|
<li><a href="{{ $loginRoute }}" class="hover:text-white transition">{{ __('messages.login') }}</a></li>
|
||||||
<li><a href="{{ $partnerRegisterRoute }}" class="hover:text-white transition">{{ __('messages.register') }}</a></li>
|
<li><a href="{{ $registerRoute }}" class="hover:text-white transition">{{ __('messages.register') }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -358,6 +345,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
@livewireScripts
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const widgetRoots = Array.from(document.querySelectorAll('[data-location-widget]'));
|
const widgetRoots = Array.from(document.querySelectorAll('[data-location-widget]'));
|
||||||
|
|||||||
13
resources/views/panel/create.blade.php
Normal file
13
resources/views/panel/create.blade.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'İlan Ver')
|
||||||
|
|
||||||
|
@section('simple_page', '1')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||||
|
<section class="bg-white border border-slate-200 rounded-xl p-0 overflow-hidden">
|
||||||
|
<livewire:panel-quick-listing-form />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
163
resources/views/panel/inbox.blade.php
Normal file
163
resources/views/panel/inbox.blade.php
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Gelen Kutusu')
|
||||||
|
|
||||||
|
@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' => 'inbox'])
|
||||||
|
|
||||||
|
<section class="bg-white border border-slate-200 rounded-xl p-0 overflow-hidden">
|
||||||
|
<div class="grid grid-cols-1 xl:grid-cols-[420px,1fr] min-h-[620px]">
|
||||||
|
<div class="border-b xl:border-b-0 xl:border-r border-slate-200">
|
||||||
|
<div class="px-6 py-5 border-b border-slate-200 flex items-center justify-between gap-3">
|
||||||
|
<h1 class="text-3xl font-bold text-slate-900">Gelen Kutusu</h1>
|
||||||
|
<svg class="w-6 h-6 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-4.35-4.35m1.6-5.05a7.25 7.25 0 11-14.5 0 7.25 7.25 0 0114.5 0z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 border-b border-slate-200">
|
||||||
|
<p class="text-sm font-semibold text-slate-600 mb-2">Hızlı Filtreler</p>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<a href="{{ route('panel.inbox.index', ['message_filter' => 'all']) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold border {{ $messageFilter === 'all' ? 'border-rose-400 bg-rose-50 text-rose-600' : 'border-slate-300 text-slate-600 hover:bg-slate-100' }}">
|
||||||
|
Hepsi
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('panel.inbox.index', ['message_filter' => 'unread']) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold border {{ $messageFilter === 'unread' ? 'border-rose-400 bg-rose-50 text-rose-600' : 'border-slate-300 text-slate-600 hover:bg-slate-100' }}">
|
||||||
|
Okunmamış
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('panel.inbox.index', ['message_filter' => 'important']) }}" class="inline-flex items-center px-4 py-2 rounded-full text-sm font-semibold border {{ $messageFilter === 'important' ? 'border-rose-400 bg-rose-50 text-rose-600' : 'border-slate-300 text-slate-600 hover:bg-slate-100' }}">
|
||||||
|
Önemli
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="max-h-[480px] overflow-y-auto divide-y divide-slate-200">
|
||||||
|
@forelse($conversations as $conversation)
|
||||||
|
@php
|
||||||
|
$conversationListing = $conversation->listing;
|
||||||
|
$partner = (int) $conversation->buyer_id === (int) auth()->id() ? $conversation->seller : $conversation->buyer;
|
||||||
|
$isSelected = $selectedConversation && (int) $selectedConversation->id === (int) $conversation->id;
|
||||||
|
$conversationImage = $conversationListing?->getFirstMediaUrl('listing-images');
|
||||||
|
$lastMessage = trim((string) ($conversation->lastMessage?->body ?? ''));
|
||||||
|
@endphp
|
||||||
|
<a href="{{ route('panel.inbox.index', ['message_filter' => $messageFilter, 'conversation' => $conversation->id]) }}" class="block px-6 py-4 transition {{ $isSelected ? 'bg-rose-50' : 'hover:bg-slate-50' }}">
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="w-14 h-14 rounded-xl bg-slate-100 border border-slate-200 overflow-hidden shrink-0">
|
||||||
|
@if($conversationImage)
|
||||||
|
<img src="{{ $conversationImage }}" alt="{{ $conversationListing?->title }}" class="w-full h-full object-cover">
|
||||||
|
@else
|
||||||
|
<div class="w-full h-full grid place-items-center text-slate-400 text-xs">İlan</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<p class="font-semibold text-2xl text-slate-900 truncate">{{ $partner?->name ?? 'Kullanıcı' }}</p>
|
||||||
|
<p class="text-xs text-slate-500 whitespace-nowrap ml-auto">{{ $conversation->last_message_at?->format('d.m.Y') }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-slate-500 truncate mt-1">{{ $conversationListing?->title ?? 'İlan silinmiş' }}</p>
|
||||||
|
<p class="text-sm {{ $conversation->unread_count > 0 ? 'text-slate-900 font-semibold' : 'text-slate-500' }} truncate mt-1">
|
||||||
|
{{ $lastMessage !== '' ? $lastMessage : 'Henüz mesaj yok' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@if($conversation->unread_count > 0)
|
||||||
|
<span class="inline-flex items-center justify-center min-w-6 h-6 px-2 rounded-full bg-rose-500 text-white text-xs font-semibold">
|
||||||
|
{{ $conversation->unread_count }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
@empty
|
||||||
|
<div class="px-6 py-16 text-center text-slate-500">
|
||||||
|
Henüz bir sohbetin yok.
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col min-h-[620px]">
|
||||||
|
@if($selectedConversation)
|
||||||
|
@php
|
||||||
|
$activeListing = $selectedConversation->listing;
|
||||||
|
$activePartner = (int) $selectedConversation->buyer_id === (int) auth()->id()
|
||||||
|
? $selectedConversation->seller
|
||||||
|
: $selectedConversation->buyer;
|
||||||
|
$activePriceLabel = $activeListing && !is_null($activeListing->price)
|
||||||
|
? number_format((float) $activeListing->price, 0).' '.($activeListing->currency ?? 'TL')
|
||||||
|
: null;
|
||||||
|
@endphp
|
||||||
|
<div class="h-24 px-6 border-b border-slate-200 flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 rounded-full bg-slate-600 text-white grid place-items-center font-semibold text-lg">
|
||||||
|
{{ strtoupper(substr((string) ($activePartner?->name ?? 'K'), 0, 1)) }}
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-3xl font-bold text-slate-900 truncate">{{ $activePartner?->name ?? 'Kullanıcı' }}</p>
|
||||||
|
<p class="text-sm text-slate-500 truncate">{{ $activeListing?->title ?? 'İlan silinmiş' }}</p>
|
||||||
|
</div>
|
||||||
|
@if($activePriceLabel)
|
||||||
|
<div class="ml-auto text-3xl font-semibold text-slate-800 whitespace-nowrap">{{ $activePriceLabel }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 px-6 py-6 bg-slate-100/60 overflow-y-auto max-h-[390px]">
|
||||||
|
@forelse($selectedConversation->messages as $message)
|
||||||
|
@php $isMine = (int) $message->sender_id === (int) auth()->id(); @endphp
|
||||||
|
<div class="mb-4 flex {{ $isMine ? 'justify-end' : 'justify-start' }}">
|
||||||
|
<div class="max-w-[80%]">
|
||||||
|
<div class="{{ $isMine ? 'bg-amber-100 text-slate-900' : 'bg-white text-slate-900 border border-slate-200' }} rounded-2xl px-4 py-2 text-base shadow-sm">
|
||||||
|
{{ $message->body }}
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-500 mt-1 {{ $isMine ? 'text-right' : 'text-left' }}">
|
||||||
|
{{ $message->created_at?->format('H:i') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
<div class="h-full grid place-items-center text-slate-500 text-center px-8">
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-slate-700">Henüz mesaj yok.</p>
|
||||||
|
<p class="text-sm mt-1">Aşağıdaki hazır metinlerden birini seçebilir veya yeni mesaj yazabilirsin.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 py-3 border-t border-slate-200 bg-white">
|
||||||
|
<div class="flex items-center gap-2 overflow-x-auto pb-2">
|
||||||
|
@foreach($quickMessages as $quickMessage)
|
||||||
|
<form method="POST" action="{{ route('conversations.messages.send', $selectedConversation) }}" class="shrink-0">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="message_filter" value="{{ $messageFilter }}">
|
||||||
|
<input type="hidden" name="message" value="{{ $quickMessage }}">
|
||||||
|
<button type="submit" class="inline-flex items-center h-11 px-5 rounded-full border border-rose-300 text-rose-600 font-semibold text-sm hover:bg-rose-50 transition">
|
||||||
|
{{ $quickMessage }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ route('conversations.messages.send', $selectedConversation) }}" class="flex items-center gap-2 border-t border-slate-200 pt-3 mt-1">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="message_filter" value="{{ $messageFilter }}">
|
||||||
|
<input type="text" name="message" value="{{ old('message') }}" placeholder="Bir mesaj yaz" maxlength="2000" class="h-12 flex-1 rounded-full border border-slate-300 px-5 text-sm focus:outline-none focus:ring-2 focus:ring-rose-300" required>
|
||||||
|
<button type="submit" class="h-12 w-12 rounded-full bg-black text-white grid place-items-center hover:bg-slate-800 transition" aria-label="Gönder">
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h13m0 0l-5-5m5 5l-5 5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@error('message')
|
||||||
|
<p class="text-xs text-rose-600 mt-2 px-2">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="h-full min-h-[620px] grid place-items-center px-8 text-center text-slate-500">
|
||||||
|
<div>
|
||||||
|
<p class="text-2xl font-semibold text-slate-700">Mesajlaşma için bir sohbet seç.</p>
|
||||||
|
<p class="mt-2 text-sm">İlan detayından veya ilan kartlarından yeni sohbet başlatabilirsin.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
143
resources/views/panel/listings.blade.php
Normal file
143
resources/views/panel/listings.blade.php
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
@extends('app::layouts.app')
|
||||||
|
|
||||||
|
@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'])
|
||||||
|
|
||||||
|
<section class="bg-white border border-slate-200 rounded-xl p-4 sm:p-6">
|
||||||
|
<div class="flex flex-col xl:flex-row xl:items-center gap-3 xl:gap-4 mb-5">
|
||||||
|
<form method="GET" action="{{ route('panel.listings.index') }}" class="relative flex-1 max-w-xl">
|
||||||
|
<svg class="w-6 h-6 text-slate-400 absolute left-4 top-1/2 -translate-y-1/2" 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="w-full h-14 rounded-2xl border border-slate-300 pl-14 pr-4 text-lg font-semibold text-slate-700 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-rose-200">
|
||||||
|
<input type="hidden" name="status" value="{{ $status }}">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'all']) }}" class="inline-flex items-center h-12 px-6 rounded-full border text-xl font-semibold {{ $status === 'all' ? 'border-rose-500 text-rose-500 bg-rose-50' : 'border-slate-300 text-slate-700 hover:bg-slate-100' }}">
|
||||||
|
Tüm İlanlar ({{ $counts['all'] }})
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'sold']) }}" class="inline-flex items-center h-12 px-6 rounded-full border text-xl font-semibold {{ $status === 'sold' ? 'border-rose-500 text-rose-500 bg-rose-50' : 'border-slate-300 text-slate-700 hover:bg-slate-100' }}">
|
||||||
|
Satıldı ({{ $counts['sold'] }})
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'expired']) }}" class="inline-flex items-center h-12 px-6 rounded-full border text-xl font-semibold {{ $status === 'expired' ? 'border-rose-500 text-rose-500 bg-rose-50' : 'border-slate-300 text-slate-700 hover:bg-slate-100' }}">
|
||||||
|
Süresi Dolmuş ({{ $counts['expired'] }})
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
@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 = match ((string) $listing->status) {
|
||||||
|
'sold' => 'Satıldı',
|
||||||
|
'expired' => 'Süresi Dolmuş',
|
||||||
|
'pending' => 'Onay Bekliyor',
|
||||||
|
default => 'Yayında',
|
||||||
|
};
|
||||||
|
$statusBadgeClass = match ((string) $listing->status) {
|
||||||
|
'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');
|
||||||
|
@endphp
|
||||||
|
<article class="rounded-2xl border border-slate-300 bg-slate-50 p-4 sm:p-5">
|
||||||
|
<div class="flex flex-col xl:flex-row gap-4 xl:items-stretch">
|
||||||
|
<div class="w-full xl:w-[260px] h-[180px] bg-slate-200 rounded-xl overflow-hidden shrink-0">
|
||||||
|
@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>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 min-w-0 flex flex-col">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<p class="text-4xl font-black text-slate-900">{{ $priceLabel }}</p>
|
||||||
|
<span class="inline-flex items-center h-10 px-4 rounded-full text-lg font-bold {{ $statusBadgeClass }}">{{ $statusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-semibold text-slate-800 mt-3 leading-tight break-words">{{ $listing->title }}</h2>
|
||||||
|
|
||||||
|
<div class="mt-auto pt-5 flex flex-wrap items-center gap-2">
|
||||||
|
<form method="POST" action="{{ route('panel.listings.destroy', $listing) }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="h-12 px-6 rounded-full border-2 border-rose-500 text-rose-500 text-2xl font-bold hover:bg-rose-50 transition">
|
||||||
|
İlanı Kaldır
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@if((string) $listing->status !== 'sold')
|
||||||
|
<form method="POST" action="{{ route('panel.listings.mark-sold', $listing) }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="h-12 px-6 rounded-full bg-rose-500 text-white text-2xl font-bold hover:bg-rose-600 transition">
|
||||||
|
Satıldı İşaretle
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if((string) $listing->status === 'expired')
|
||||||
|
<form method="POST" action="{{ route('panel.listings.republish', $listing) }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="h-12 px-6 rounded-full border-2 border-rose-500 text-rose-500 text-2xl font-bold hover:bg-rose-50 transition">
|
||||||
|
Yeniden Yayınla
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="xl:w-[260px] flex xl:flex-col items-start xl:items-end justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="h-12 min-w-24 px-4 rounded-2xl bg-slate-200 text-slate-500 text-xl font-bold inline-flex items-center justify-center gap-2">
|
||||||
|
<span>👁</span>
|
||||||
|
<span>{{ $viewCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-12 min-w-24 px-4 rounded-2xl bg-slate-200 text-slate-500 text-xl font-bold inline-flex items-center justify-center gap-2">
|
||||||
|
<span>♥</span>
|
||||||
|
<span>{{ $favoriteCount }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-lg text-slate-500 text-left xl:text-right">
|
||||||
|
Yayın Tarihi & Bitiş Tarihi:
|
||||||
|
<strong class="text-slate-700">
|
||||||
|
{{ $listing->created_at?->format('d/m/Y') ?? '-' }} - {{ $expiresAt ?: '-' }}
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if((string) $listing->status === 'expired')
|
||||||
|
<div class="mt-4 rounded-xl bg-sky-100 px-4 py-3 text-base text-slate-700">
|
||||||
|
<strong>Bu ilanın süresi doldu.</strong> Eğer sattıysan, lütfen satıldı olarak işaretle.
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</article>
|
||||||
|
@empty
|
||||||
|
<div class="rounded-xl border border-dashed border-slate-300 py-16 text-center text-slate-500">
|
||||||
|
Bu filtreye uygun ilan bulunamadı.
|
||||||
|
</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($listings->hasPages())
|
||||||
|
<div class="mt-5">
|
||||||
|
{{ $listings->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
28
resources/views/panel/partials/sidebar.blade.php
Normal file
28
resources/views/panel/partials/sidebar.blade.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
@php
|
||||||
|
$activeMenu = $activeMenu ?? '';
|
||||||
|
$activeFavoritesTab = $activeFavoritesTab ?? '';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<aside class="bg-white border border-slate-200 rounded-xl overflow-hidden">
|
||||||
|
<a href="{{ route('panel.listings.create') }}" class="block px-5 py-4 text-base {{ $activeMenu === 'create' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-700 hover:bg-slate-50' }}">
|
||||||
|
İlan Ver
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('panel.listings.index') }}" class="block px-5 py-4 border-t border-slate-200 text-base {{ $activeMenu === 'listings' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-700 hover:bg-slate-50' }}">
|
||||||
|
İlanlarım
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('favorites.index', ['tab' => 'listings']) }}" class="block px-5 py-4 border-t border-slate-200 text-base {{ $activeMenu === 'favorites' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-700 hover:bg-slate-50' }}">
|
||||||
|
Favorilerim
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('favorites.index', ['tab' => 'listings']) }}" class="block px-9 py-3 border-t border-slate-100 text-sm {{ $activeFavoritesTab === 'listings' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||||
|
Favori İlanlar
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('favorites.index', ['tab' => 'searches']) }}" class="block px-9 py-3 border-t border-slate-100 text-sm {{ $activeFavoritesTab === 'searches' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||||
|
Favori Aramalar
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('favorites.index', ['tab' => 'sellers']) }}" class="block px-9 py-3 border-t border-slate-100 text-sm {{ $activeFavoritesTab === 'sellers' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||||
|
Favori Satıcılar
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('panel.inbox.index') }}" class="block px-5 py-4 border-t border-slate-200 text-base {{ $activeMenu === 'inbox' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-700 hover:bg-slate-50' }}">
|
||||||
|
Gelen Kutusu
|
||||||
|
</a>
|
||||||
|
</aside>
|
||||||
1287
resources/views/panel/quick-create.blade.php
Normal file
1287
resources/views/panel/quick-create.blade.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,16 +7,18 @@ use App\Http\Controllers\Auth\EmailVerificationPromptController;
|
|||||||
use App\Http\Controllers\Auth\NewPasswordController;
|
use App\Http\Controllers\Auth\NewPasswordController;
|
||||||
use App\Http\Controllers\Auth\PasswordController;
|
use App\Http\Controllers\Auth\PasswordController;
|
||||||
use App\Http\Controllers\Auth\PasswordResetLinkController;
|
use App\Http\Controllers\Auth\PasswordResetLinkController;
|
||||||
use App\Http\Controllers\Auth\PartnerAuthGatewayController;
|
use App\Http\Controllers\Auth\RegisteredUserController;
|
||||||
use App\Http\Controllers\Auth\VerifyEmailController;
|
use App\Http\Controllers\Auth\VerifyEmailController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::middleware('guest')->group(function () {
|
Route::middleware('guest')->group(function () {
|
||||||
Route::get('register', [PartnerAuthGatewayController::class, 'register'])
|
Route::get('register', [RegisteredUserController::class, 'create'])
|
||||||
->name('register');
|
->name('register');
|
||||||
|
Route::post('register', [RegisteredUserController::class, 'store']);
|
||||||
|
|
||||||
Route::get('login', [PartnerAuthGatewayController::class, 'login'])
|
Route::get('login', [AuthenticatedSessionController::class, 'create'])
|
||||||
->name('login');
|
->name('login');
|
||||||
|
Route::post('login', [AuthenticatedSessionController::class, 'store']);
|
||||||
|
|
||||||
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
|
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
|
||||||
->name('password.request');
|
->name('password.request');
|
||||||
|
|||||||
@ -4,29 +4,28 @@ use App\Http\Controllers\ConversationController;
|
|||||||
use App\Http\Controllers\FavoriteController;
|
use App\Http\Controllers\FavoriteController;
|
||||||
use App\Http\Controllers\HomeController;
|
use App\Http\Controllers\HomeController;
|
||||||
use App\Http\Controllers\LanguageController;
|
use App\Http\Controllers\LanguageController;
|
||||||
|
use App\Http\Controllers\PanelController;
|
||||||
|
|
||||||
Route::get('/', [HomeController::class, 'index'])->name('home');
|
Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||||
Route::get('/lang/{locale}', [LanguageController::class, 'switch'])->name('lang.switch');
|
Route::get('/lang/{locale}', [LanguageController::class, 'switch'])->name('lang.switch');
|
||||||
|
|
||||||
$redirectToPartner = static function (string $routeName) {
|
Route::get('/dashboard', fn () => auth()->check()
|
||||||
if (! auth()->check()) {
|
? redirect()->route('panel.listings.index')
|
||||||
return redirect()->route('filament.partner.auth.login');
|
: redirect()->route('login'))
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->route($routeName, ['tenant' => auth()->id()]);
|
|
||||||
};
|
|
||||||
|
|
||||||
Route::get('/dashboard', fn () => $redirectToPartner('filament.partner.pages.dashboard'))
|
|
||||||
->name('dashboard');
|
->name('dashboard');
|
||||||
|
|
||||||
Route::get('/partner', fn () => $redirectToPartner('filament.partner.pages.dashboard'))
|
Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
|
||||||
->name('partner.dashboard');
|
Route::get('/', [PanelController::class, 'index'])->name('index');
|
||||||
|
Route::get('/ilanlarim', [PanelController::class, 'listings'])->name('listings.index');
|
||||||
|
Route::get('/ilan-ver', [PanelController::class, 'create'])->name('listings.create');
|
||||||
|
Route::get('/gelen-kutusu', [PanelController::class, 'inbox'])->name('inbox.index');
|
||||||
|
Route::post('/ilanlarim/{listing}/kaldir', [PanelController::class, 'destroyListing'])->name('listings.destroy');
|
||||||
|
Route::post('/ilanlarim/{listing}/satildi', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold');
|
||||||
|
Route::post('/ilanlarim/{listing}/yeniden-yayinla', [PanelController::class, 'republishListing'])->name('listings.republish');
|
||||||
|
});
|
||||||
|
|
||||||
Route::get('/partner/listings', fn () => $redirectToPartner('filament.partner.resources.listings.index'))
|
Route::get('/partner/{any?}', fn () => redirect()->route('panel.listings.index'))
|
||||||
->name('partner.listings.index');
|
->where('any', '.*');
|
||||||
|
|
||||||
Route::get('/partner/listings/create', fn () => $redirectToPartner('filament.partner.resources.listings.create'))
|
|
||||||
->name('partner.listings.create');
|
|
||||||
|
|
||||||
Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
|
Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
|
||||||
Route::get('/', [FavoriteController::class, 'index'])->name('index');
|
Route::get('/', [FavoriteController::class, 'index'])->name('index');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user