Compare commits

...

4 Commits

Author SHA1 Message Date
Fatih Alp
568368bb17
Merge pull request #1392 from openclassify/fatihalp-patch-1
Delete .agents/skills/laravel-permission-development/SKILL.md
2026-03-04 00:30:14 +03:00
Fatih Alp
6600422505
Delete .agents/skills/laravel-permission-development/SKILL.md 2026-03-04 00:30:03 +03:00
fatihalp
747e7410f4 Update partner listings flow 2026-03-04 00:29:08 +03:00
fatihalp
57fa68fdfe Kur önyüzde birleşik ilan paneli 2026-03-03 22:56:00 +03:00
32 changed files with 2621 additions and 525 deletions

View File

@ -1,277 +0,0 @@
---
name: laravel-permission-development
description: Build and work with Spatie Laravel Permission features, including roles, permissions, middleware, policies, teams, and Blade directives.
---
# Laravel Permission Development
## When to use this skill
Use this skill when working with authorization, roles, permissions, access control, middleware guards, or Blade permission directives using spatie/laravel-permission.
## Core Concepts
- **Users have Roles, Roles have Permissions, Apps check Permissions** (not Roles).
- Direct permissions on users are an anti-pattern; assign permissions to roles instead.
- Use `$user->can('permission-name')` for all authorization checks (supports Super Admin via Gate).
- The `HasRoles` trait (which includes `HasPermissions`) is added to User models.
## Setup
Add the `HasRoles` trait to your User model:
```php
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use HasRoles;
}
```
## Creating Roles and Permissions
```php
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
$role = Role::create(['name' => 'writer']);
$permission = Permission::create(['name' => 'edit articles']);
// findOrCreate is idempotent (safe for seeders)
$role = Role::findOrCreate('writer', 'web');
$permission = Permission::findOrCreate('edit articles', 'web');
```
## Assigning Roles and Permissions
```php
// Assign roles to users
$user->assignRole('writer');
$user->assignRole('writer', 'admin');
$user->assignRole(['writer', 'admin']);
$user->syncRoles(['writer', 'admin']); // replaces all
$user->removeRole('writer');
// Assign permissions to roles (preferred)
$role->givePermissionTo('edit articles');
$role->givePermissionTo(['edit articles', 'delete articles']);
$role->syncPermissions(['edit articles', 'delete articles']);
$role->revokePermissionTo('edit articles');
// Reverse assignment
$permission->assignRole('writer');
$permission->syncRoles(['writer', 'editor']);
$permission->removeRole('writer');
```
## Checking Roles and Permissions
```php
// Permission checks (preferred - supports Super Admin via Gate)
$user->can('edit articles');
$user->canAny(['edit articles', 'delete articles']);
// Direct package methods (bypass Gate, no Super Admin support)
$user->hasPermissionTo('edit articles');
$user->hasAnyPermission(['edit articles', 'publish articles']);
$user->hasAllPermissions(['edit articles', 'publish articles']);
$user->hasDirectPermission('edit articles');
// Role checks
$user->hasRole('writer');
$user->hasAnyRole(['writer', 'editor']);
$user->hasAllRoles(['writer', 'editor']);
$user->hasExactRoles(['writer', 'editor']);
// Get assigned roles and permissions
$user->getRoleNames(); // Collection of role name strings
$user->getPermissionNames(); // Collection of permission name strings
$user->getDirectPermissions(); // Direct permissions only
$user->getPermissionsViaRoles(); // Inherited via roles
$user->getAllPermissions(); // Both direct and inherited
```
## Query Scopes
```php
$users = User::role('writer')->get();
$users = User::withoutRole('writer')->get();
$users = User::permission('edit articles')->get();
$users = User::withoutPermission('edit articles')->get();
```
## Middleware
Register middleware aliases in `bootstrap/app.php`:
```php
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
]);
})
```
Use in routes (pipe `|` for OR logic):
```php
Route::middleware(['permission:edit articles'])->group(function () { ... });
Route::middleware(['role:manager|writer'])->group(function () { ... });
Route::middleware(['role_or_permission:manager|edit articles'])->group(function () { ... });
// With specific guard
Route::middleware(['role:manager,api'])->group(function () { ... });
```
For single permissions, Laravel's built-in `can` middleware also works:
```php
Route::middleware(['can:edit articles'])->group(function () { ... });
```
## Blade Directives
Prefer `@can` (permission-based) over `@role` (role-based):
```blade
@can('edit articles')
{{-- User can edit articles (supports Super Admin) --}}
@endcan
@canany(['edit articles', 'delete articles'])
{{-- User can do at least one --}}
@endcanany
@role('admin')
{{-- Only use for super-admin type checks --}}
@endrole
@hasanyrole('writer|admin')
{{-- Has writer or admin --}}
@endhasanyrole
```
## Super Admin
Use `Gate::before` in `AppServiceProvider::boot()`:
```php
use Illuminate\Support\Facades\Gate;
public function boot(): void
{
Gate::before(function ($user, $ability) {
return $user->hasRole('Super Admin') ? true : null;
});
}
```
This makes `$user->can()` and `@can` always return true for Super Admins. Must return `null` (not `false`) to allow normal checks for other users.
## Policies
Use `$user->can()` inside policy methods to check permissions:
```php
class PostPolicy
{
public function update(User $user, Post $post): bool
{
if ($user->can('edit all posts')) {
return true;
}
return $user->can('edit own posts') && $user->id === $post->user_id;
}
}
```
## Enums
```php
enum RolesEnum: string
{
case WRITER = 'writer';
case EDITOR = 'editor';
}
enum PermissionsEnum: string
{
case EDIT_POSTS = 'edit posts';
case DELETE_POSTS = 'delete posts';
}
// Creation requires ->value
Permission::findOrCreate(PermissionsEnum::EDIT_POSTS->value, 'web');
// Most methods accept enums directly
$user->assignRole(RolesEnum::WRITER);
$user->hasRole(RolesEnum::WRITER);
$role->givePermissionTo(PermissionsEnum::EDIT_POSTS);
$user->hasPermissionTo(PermissionsEnum::EDIT_POSTS);
```
## Seeding
Always flush the permission cache when seeding:
```php
class RolesAndPermissionsSeeder extends Seeder
{
public function run(): void
{
// Reset cache
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// Create permissions
Permission::findOrCreate('edit articles', 'web');
Permission::findOrCreate('delete articles', 'web');
// Create roles and assign permissions
Role::findOrCreate('writer', 'web')
->givePermissionTo(['edit articles']);
Role::findOrCreate('admin', 'web')
->givePermissionTo(Permission::all());
}
}
```
## Teams (Multi-Tenancy)
Enable in `config/permission.php` before running migrations:
```php
'teams' => true,
```
Set the active team in middleware:
```php
setPermissionsTeamId($teamId);
```
When switching teams, unset cached relations:
```php
$user->unsetRelation('roles')->unsetRelation('permissions');
```
## Events
Enable in `config/permission.php`:
```php
'events_enabled' => true,
```
Available events: `RoleAttachedEvent`, `RoleDetachedEvent`, `PermissionAttachedEvent`, `PermissionDetachedEvent` in the `Spatie\Permission\Events` namespace.
## Performance
- Permissions are cached automatically. The cache is flushed when roles/permissions change via package methods.
- After direct DB operations, flush manually: `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions()`
- For bulk seeding, use `Permission::insert()` for speed, but flush the cache afterward.

View File

@ -1,12 +0,0 @@
name: Issues
on:
issues:
types: [labeled]
permissions:
issues: write
jobs:
help-wanted:
uses: laravel/.github/.github/workflows/issues.yml@main

View File

@ -1,12 +0,0 @@
name: Pull Requests
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
jobs:
uneditable:
uses: laravel/.github/.github/workflows/pull-requests.yml@main

View File

@ -1,13 +0,0 @@
name: Update Changelog
on:
release:
types: [released]
permissions: {}
jobs:
update:
permissions:
contents: write
uses: laravel/.github/.github/workflows/update-changelog.yml@main

View File

@ -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');
}
});
}
};

View File

@ -4,6 +4,7 @@ namespace Modules\Listing\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Conversation;
use App\Models\FavoriteSearch;
use Illuminate\Support\Facades\Schema;
use Modules\Category\Models\Category;
use Modules\Listing\Models\Listing;
use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
@ -82,6 +83,14 @@ class ListingController extends Controller
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');
$presentableCustomFields = ListingCustomFieldSchemaBuilder::presentableValues(
$listing->category_id ? (int) $listing->category_id : null,
@ -127,20 +136,20 @@ class ListingController extends Controller
public function create()
{
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()
{
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()])
->with('success', 'Use the Partner Panel to create listings.');
->route('panel.listings.create')
->with('success', 'İlan oluşturma ekranına yönlendirildin.');
}
}

View File

@ -22,13 +22,14 @@ class Listing extends Model implements HasMedia
'title', 'description', 'price', 'currency', 'category_id',
'user_id', 'status', 'images', 'custom_fields', 'slug',
'contact_phone', 'contact_email', 'is_featured', 'expires_at',
'city', 'country', 'latitude', 'longitude', 'location',
'city', 'country', 'latitude', 'longitude', 'location', 'view_count',
];
protected $casts = [
'images' => 'array',
'custom_fields' => 'array',
'is_featured' => 'boolean',
'view_count' => 'integer',
'expires_at' => 'datetime',
'price' => 'decimal:2',
'latitude' => 'decimal:7',

View File

@ -69,7 +69,7 @@
</button>
</form>
@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>
@endauth
@ -90,7 +90,7 @@
@auth
@if($listing->user_id && (int) $listing->user_id !== (int) auth()->id())
@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
</a>
@else

View File

@ -53,7 +53,7 @@
</button>
</form>
@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
</a>
@else
@ -66,7 +66,7 @@
@endif
@endif
@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
</a>
@endauth

View File

@ -7,8 +7,5 @@ class PartnerServiceProvider extends ServiceProvider
{
public function boot(): void {}
public function register(): void
{
$this->app->register(PartnerPanelProvider::class);
}
public function register(): void {}
}

View File

@ -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);
}
}

View File

@ -53,8 +53,8 @@ class ConversationController extends Controller
}
return redirect()
->route('favorites.index', array_merge(
$this->listingTabFilters($request),
->route('panel.inbox.index', array_merge(
$this->inboxFilters($request),
['conversation' => $conversation->getKey()],
))
->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.');
@ -83,28 +83,16 @@ class ConversationController extends Controller
])->save();
return redirect()
->route('favorites.index', array_merge(
$this->listingTabFilters($request),
->route('panel.inbox.index', array_merge(
$this->inboxFilters($request),
['conversation' => $conversation->getKey()],
))
->with('success', 'Mesaj gönderildi.');
}
private function listingTabFilters(Request $request): array
private function inboxFilters(Request $request): array
{
$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;
}
$filters = [];
$messageFilter = (string) $request->string('message_filter');
if (in_array($messageFilter, ['all', 'unread', 'important'], true)) {

View 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);
}
}
}

View 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);
}
}

View File

@ -48,7 +48,6 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata
{
return match ($panel->getId()) {
'admin' => $this->hasRole('admin'),
'partner' => true,
default => false,
};
}

View File

@ -8,7 +8,6 @@ use BezhanSalleh\LanguageSwitch\LanguageSwitch;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\View;
@ -33,25 +32,8 @@ class AppServiceProvider extends ServiceProvider
return null;
});
Route::pattern('tenant', '[0-9]+');
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');
$fallbackLocale = config('app.locale', 'en');
$fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD']));

View File

@ -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"));
}
}

View File

@ -3,5 +3,4 @@
return [
App\Providers\AppServiceProvider::class,
Modules\Admin\Providers\AdminPanelProvider::class,
Modules\Partner\Providers\PartnerPanelProvider::class,
];

View File

@ -4,5 +4,5 @@
"Location": true,
"Profile": true,
"Admin": true,
"Partner": true
"Partner": false
}

View File

@ -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">
Back Home
</a>
<a href="{{ route('filament.partner.auth.login') }}" class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700">
Partner Login
<a href="{{ route('login') }}" class="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700">
Giriş Yap
</a>
</div>
</div>

View File

@ -18,14 +18,14 @@
</a>
@auth
<form method="POST" action="{{ route('filament.partner.auth.logout') }}">
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="px-4 py-2 rounded-lg bg-red-600 text-white hover:bg-red-700">
Çıkış Yap
</button>
</form>
@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
</a>
@endauth

View File

@ -5,17 +5,7 @@
@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">
<aside class="bg-white border border-slate-200">
<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>
@include('panel.partials.sidebar', ['activeMenu' => 'favorites', 'activeFavoritesTab' => $activeTab])
<section class="bg-white border border-slate-200">
@if($activeTab === 'listings')
@ -98,7 +88,7 @@
<td class="px-4 py-4">
@if($canMessageListing)
@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
</a>
@else

View File

@ -1,4 +1,4 @@
<x-filament-panels::page>
<div class="max-w-[1320px] mx-auto px-4 py-8">
<style>
.qc-shell {
--qc-bg: #ededed;
@ -1257,7 +1257,7 @@
<span class="qc-avatar">{{ $this->currentUserInitial }}</span>
<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>
@ -1284,4 +1284,4 @@
@endif
</div>
</div>
</x-filament-panels::page>
</div>

View File

@ -35,11 +35,11 @@
İncele
</a>
@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') }}
</a>
@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') }}
</a>
@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>
</form>
@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
</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>
</div>
@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
</a>
@else

View File

@ -7,16 +7,13 @@
$whatsappNumber = $generalSettings['whatsapp'] ?? null;
$whatsappDigits = preg_replace('/\D+/', '', (string) $whatsappNumber);
$whatsappUrl = $whatsappDigits !== '' ? 'https://wa.me/' . $whatsappDigits : null;
$partnerLoginRoute = route('filament.partner.auth.login');
$partnerRegisterRoute = route('register');
$partnerLogoutRoute = route('filament.partner.auth.logout');
$partnerCreateRoute = route('partner.listings.create');
$partnerQuickCreateRoute = auth()->check()
? route('filament.partner.resources.listings.quick-create', ['tenant' => auth()->id()])
: $partnerLoginRoute;
$partnerDashboardRoute = auth()->check()
? route('filament.partner.pages.dashboard', ['tenant' => auth()->id()])
: $partnerLoginRoute;
$loginRoute = route('login');
$registerRoute = route('register');
$logoutRoute = route('logout');
$panelCreateRoute = auth()->check() ? route('panel.listings.create') : $loginRoute;
$panelListingsRoute = auth()->check() ? route('panel.listings.index') : $loginRoute;
$inboxRoute = auth()->check() ? route('panel.inbox.index') : $loginRoute;
$favoritesRoute = auth()->check() ? route('favorites.index') : $loginRoute;
$availableLocales = config('app.available_locales', ['en']);
$localeLabels = [
'en' => 'English',
@ -31,6 +28,7 @@
'ja' => '日本語',
];
$isHomePage = request()->routeIs('home');
$isSimplePage = trim($__env->yieldContent('simple_page')) === '1';
$homeHeaderCategories = isset($categories) ? collect($categories)->take(8) : collect();
$locationCountries = collect($headerLocationCountries ?? [])->values();
$defaultCountryIso2 = strtoupper((string) config('app.default_country_iso2', 'TR'));
@ -132,6 +130,7 @@
text-align: right;
}
</style>
@livewireStyles
</head>
<body class="min-h-screen">
<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">
@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">
<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>
</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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 12l9-9 9 9M5 10v10h14V10"/>
</svg>
</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">
Post Fast
<a href="{{ $panelCreateRoute }}" class="btn-primary px-4 md:px-5 py-2.5 text-sm font-semibold shadow-sm hover:brightness-95 transition">
İlan Ver
</a>
<details class="relative">
<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">
<form method="POST" action="{{ $logoutRoute }}" class="hidden xl:block">
@csrf
<button type="submit" class="text-sm text-slate-500 hover:text-rose-500 transition">{{ __('messages.logout') }}</button>
</form>
@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">
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">
<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">
{{ __('messages.login') }}
</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">
Sat
<a href="{{ $panelCreateRoute }}" class="btn-primary px-4 md:px-5 py-2.5 text-sm font-semibold shadow-sm hover:brightness-95 transition">
İlan Ver
</a>
@endauth
</div>
@ -267,11 +254,11 @@
</form>
<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>
<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>
@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="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">
@ -287,7 +274,7 @@
@endforeach
</div>
</div>
@elseif(! $isHomePage)
@elseif(! $isSimplePage && ! $isHomePage)
<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('categories.index') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.categories') }}</a>
@ -325,8 +312,8 @@
<div>
<h4 class="text-white font-medium mb-4">Hesap</h4>
<ul class="space-y-2 text-sm">
<li><a href="{{ $partnerLoginRoute }}" 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="{{ $loginRoute }}" class="hover:text-white transition">{{ __('messages.login') }}</a></li>
<li><a href="{{ $registerRoute }}" class="hover:text-white transition">{{ __('messages.register') }}</a></li>
</ul>
</div>
<div>
@ -358,6 +345,7 @@
</div>
</div>
</footer>
@livewireScripts
<script>
(() => {
const widgetRoots = Array.from(document.querySelectorAll('[data-location-widget]'));

View 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

View 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

View 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

View 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>

File diff suppressed because it is too large Load Diff

View File

@ -7,16 +7,18 @@ 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\PartnerAuthGatewayController;
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', [PartnerAuthGatewayController::class, 'register'])
Route::get('register', [RegisteredUserController::class, 'create'])
->name('register');
Route::post('register', [RegisteredUserController::class, 'store']);
Route::get('login', [PartnerAuthGatewayController::class, 'login'])
Route::get('login', [AuthenticatedSessionController::class, 'create'])
->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']);
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');

View File

@ -4,29 +4,28 @@ use App\Http\Controllers\ConversationController;
use App\Http\Controllers\FavoriteController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\LanguageController;
use App\Http\Controllers\PanelController;
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/lang/{locale}', [LanguageController::class, 'switch'])->name('lang.switch');
$redirectToPartner = static function (string $routeName) {
if (! auth()->check()) {
return redirect()->route('filament.partner.auth.login');
}
return redirect()->route($routeName, ['tenant' => auth()->id()]);
};
Route::get('/dashboard', fn () => $redirectToPartner('filament.partner.pages.dashboard'))
Route::get('/dashboard', fn () => auth()->check()
? redirect()->route('panel.listings.index')
: redirect()->route('login'))
->name('dashboard');
Route::get('/partner', fn () => $redirectToPartner('filament.partner.pages.dashboard'))
->name('partner.dashboard');
Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
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'))
->name('partner.listings.index');
Route::get('/partner/listings/create', fn () => $redirectToPartner('filament.partner.resources.listings.create'))
->name('partner.listings.create');
Route::get('/partner/{any?}', fn () => redirect()->route('panel.listings.index'))
->where('any', '.*');
Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
Route::get('/', [FavoriteController::class, 'index'])->name('index');