mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
Compare commits
4 Commits
d603de62ec
...
568368bb17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
568368bb17 | ||
|
|
6600422505 | ||
|
|
747e7410f4 | ||
|
|
57fa68fdfe |
@ -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.
|
||||
12
.github/workflows/issues.yml
vendored
12
.github/workflows/issues.yml
vendored
@ -1,12 +0,0 @@
|
||||
name: Issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
help-wanted:
|
||||
uses: laravel/.github/.github/workflows/issues.yml@main
|
||||
12
.github/workflows/pull-requests.yml
vendored
12
.github/workflows/pull-requests.yml
vendored
@ -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
|
||||
13
.github/workflows/update-changelog.yml
vendored
13
.github/workflows/update-changelog.yml
vendored
@ -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
|
||||
@ -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\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.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
->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)) {
|
||||
|
||||
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()) {
|
||||
'admin' => $this->hasRole('admin'),
|
||||
'partner' => true,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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']));
|
||||
|
||||
@ -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 [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
Modules\Admin\Providers\AdminPanelProvider::class,
|
||||
Modules\Partner\Providers\PartnerPanelProvider::class,
|
||||
];
|
||||
|
||||
@ -4,5 +4,5 @@
|
||||
"Location": true,
|
||||
"Profile": 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">
|
||||
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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]'));
|
||||
|
||||
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\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');
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user