Konsolide et Filament V5 partner kim

This commit is contained in:
fatihalp 2026-03-03 17:29:59 +03:00
parent 40b5e0d01a
commit 3428bd3e43
15 changed files with 165 additions and 239 deletions

View File

@ -2,10 +2,7 @@
namespace Modules\Listing\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Modules\Listing\Models\Listing;
use Modules\Listing\Support\ListingPanelHelper;
class ListingController extends Controller
{
@ -24,29 +21,21 @@ class ListingController extends Controller
public function create()
{
return view('listing::create', [
'currencies' => ListingPanelHelper::currencyCodes(),
]);
if (! auth()->check()) {
return redirect()->route('filament.partner.auth.login');
}
return redirect()->route('filament.partner.resources.listings.create', ['tenant' => auth()->id()]);
}
public function store(Request $request)
public function store()
{
$currencies = ListingPanelHelper::currencyCodes();
if (! auth()->check()) {
return redirect()->route('filament.partner.auth.login');
}
$data = $request->validate([
'title' => 'required|string|min:3|max:255',
'description' => 'nullable|string',
'price' => 'nullable|numeric|min:0',
'currency' => ['nullable', 'string', 'size:3', Rule::in($currencies)],
'city' => 'nullable|string|max:120',
'country' => 'nullable|string|max:120',
'category_id' => 'nullable|integer',
'contact_email' => 'nullable|email',
'contact_phone' => 'nullable|string',
]);
$listing = Listing::createFromFrontend($data, auth()->id());
return redirect()->route('listings.show', $listing)->with('success', 'Listing created!');
return redirect()
->route('filament.partner.resources.listings.create', ['tenant' => auth()->id()])
->with('success', 'Use the Partner Panel to create listings.');
}
}

View File

@ -1,56 +0,0 @@
@extends('app::layouts.app')
@section('content')
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<h1 class="text-2xl font-bold mb-6">Post a New Listing</h1>
<form method="POST" action="{{ route('listings.store') }}" class="bg-white rounded-lg shadow-md p-6 space-y-4">
@csrf
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Title *</label>
<input type="text" name="title" value="{{ old('title') }}" required class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
@error('title')<p class="text-red-500 text-sm mt-1">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea name="description" rows="4" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('description') }}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Price</label>
<input type="number" name="price" value="{{ old('price') }}" step="0.01" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
@error('price')<p class="text-red-500 text-sm mt-1">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Currency</label>
<select name="currency" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
@php($defaultCurrency = old('currency', $currencies[0] ?? 'USD'))
@foreach(($currencies ?? ['USD']) as $currency)
<option value="{{ $currency }}" @selected($defaultCurrency === $currency)>{{ $currency }}</option>
@endforeach
</select>
@error('currency')<p class="text-red-500 text-sm mt-1">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">City</label>
<input type="text" name="city" value="{{ old('city') }}" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
@error('city')<p class="text-red-500 text-sm mt-1">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Country</label>
<input type="text" name="country" value="{{ old('country') }}" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
@error('country')<p class="text-red-500 text-sm mt-1">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Contact Email</label>
<input type="email" name="contact_email" value="{{ old('contact_email') }}" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
@error('contact_email')<p class="text-red-500 text-sm mt-1">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Contact Phone</label>
<input type="text" name="contact_phone" value="{{ old('contact_phone') }}" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
@error('contact_phone')<p class="text-red-500 text-sm mt-1">{{ $message }}</p>@enderror
</div>
<button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition font-medium">Post Listing</button>
</form>
</div>
</div>
@endsection

View File

@ -5,6 +5,6 @@ use Modules\Listing\Http\Controllers\ListingController;
Route::middleware('web')->prefix('listings')->name('listings.')->group(function () {
Route::get('/', [ListingController::class, 'index'])->name('index');
Route::get('/create', [ListingController::class, 'create'])->name('create');
Route::post('/', [ListingController::class, 'store'])->name('store')->middleware('auth');
Route::post('/', [ListingController::class, 'store'])->name('store');
Route::get('/{listing}', [ListingController::class, 'show'])->name('show');
});

View File

@ -0,0 +1,27 @@
<?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

@ -1,18 +0,0 @@
<?php
namespace App\Http\Controllers\Partner;
use App\Http\Controllers\Controller;
use Modules\Listing\Models\Listing;
class DashboardController extends Controller
{
public function index()
{
$myListings = Listing::where('user_id', auth()->id())->latest()->paginate(10);
$stats = [
'total' => Listing::where('user_id', auth()->id())->count(),
'active' => Listing::where('user_id', auth()->id())->where('status', 'active')->count(),
];
return view('partner.dashboard', compact('myListings', 'stats'));
}
}

View File

@ -1,14 +0,0 @@
<?php
namespace App\Http\Controllers\Partner;
use App\Http\Controllers\Controller;
use Modules\Listing\Models\Listing;
class ListingController extends Controller
{
public function index()
{
$listings = Listing::where('user_id', auth()->id())->latest()->paginate(15);
return view('partner.listings.index', compact('listings'));
}
}

View File

@ -7,6 +7,7 @@ use App\Settings\GeneralSettings;
use BezhanSalleh\LanguageSwitch\LanguageSwitch;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\View;
@ -22,8 +23,25 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
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

@ -0,0 +1,48 @@
<?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

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

View File

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

View File

@ -7,6 +7,15 @@
$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 = auth()->check()
? route('filament.partner.resources.listings.create', ['tenant' => auth()->id()])
: $partnerLoginRoute;
$partnerDashboardRoute = auth()->check()
? route('filament.partner.pages.dashboard', ['tenant' => auth()->id()])
: $partnerLoginRoute;
@endphp
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" dir="{{ in_array(app()->getLocale(), ['ar']) ? 'rtl' : 'ltr' }}">
@ -19,11 +28,6 @@
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>body { font-family: 'Inter', sans-serif; } [dir="rtl"] { text-align: right; }</style>
</head>
@php
$partnerCreateRoute = auth()->check() && \Illuminate\Support\Facades\Route::has('filament.partner.resources.listings.create')
? route('filament.partner.resources.listings.create', ['tenant' => auth()->id()])
: route('listings.create');
@endphp
<body class="bg-gray-50">
<nav class="bg-white shadow-sm border-b sticky top-0 z-50">
<div class="container mx-auto px-4">
@ -58,16 +62,16 @@
<button class="text-gray-600 hover:text-blue-600">{{ auth()->user()->name }}</button>
<div class="absolute right-0 mt-1 bg-white shadow-lg rounded-lg border hidden group-hover:block z-50 w-40">
<a href="{{ route('profile.show') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">My Profile</a>
<a href="{{ route('partner.dashboard') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Dashboard</a>
<form method="POST" action="{{ route('logout') }}">
<a href="{{ $partnerDashboardRoute }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Dashboard</a>
<form method="POST" action="{{ $partnerLogoutRoute }}">
@csrf
<button type="submit" class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-50">Logout</button>
</form>
</div>
</div>
@else
<a href="{{ route('login') }}" class="text-gray-600 hover:text-blue-600 transition">{{ __('messages.login') }}</a>
<a href="{{ route('register') }}" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition text-sm">{{ __('messages.register') }}</a>
<a href="{{ $partnerLoginRoute }}" class="text-gray-600 hover:text-blue-600 transition">{{ __('messages.login') }}</a>
<a href="{{ $partnerRegisterRoute }}" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition text-sm">{{ __('messages.register') }}</a>
@endauth
</div>
</div>
@ -98,8 +102,8 @@
<div>
<h4 class="text-white font-medium mb-4">Account</h4>
<ul class="space-y-2 text-sm">
<li><a href="{{ route('login') }}" class="hover:text-white transition">Login</a></li>
<li><a href="{{ route('register') }}" class="hover:text-white transition">Register</a></li>
<li><a href="{{ $partnerLoginRoute }}" class="hover:text-white transition">Login</a></li>
<li><a href="{{ $partnerRegisterRoute }}" class="hover:text-white transition">Register</a></li>
</ul>
</div>
<div>

View File

@ -1,59 +0,0 @@
@extends('app::layouts.app')
@section('title', 'Dashboard')
@section('content')
@php($partnerCreateRoute = route('filament.partner.resources.listings.create', ['tenant' => auth()->id()]))
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">My Dashboard</h1>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div class="text-gray-500 text-sm font-medium">Total Listings</div>
<div class="text-3xl font-bold text-blue-600 mt-1">{{ $stats['total'] }}</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div class="text-gray-500 text-sm font-medium">Active Listings</div>
<div class="text-3xl font-bold text-green-600 mt-1">{{ $stats['active'] }}</div>
</div>
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
<div class="text-gray-500 text-sm font-medium">Quick Actions</div>
<a href="{{ $partnerCreateRoute }}" class="mt-2 block text-center bg-orange-500 text-white py-2 rounded-lg hover:bg-orange-600 transition text-sm">+ Post New Listing</a>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b flex justify-between items-center">
<h2 class="font-semibold text-gray-900">My Listings</h2>
<a href="{{ route('partner.listings.index') }}" class="text-blue-600 text-sm hover:underline">View all</a>
</div>
<table class="w-full">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-6 py-3 text-xs font-medium text-gray-500 uppercase">Title</th>
<th class="text-left px-6 py-3 text-xs font-medium text-gray-500 uppercase">Price</th>
<th class="text-left px-6 py-3 text-xs font-medium text-gray-500 uppercase">Status</th>
<th class="text-left px-6 py-3 text-xs font-medium text-gray-500 uppercase">Date</th>
<th class="text-left px-6 py-3 text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($myListings as $listing)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 font-medium text-gray-900">{{ \Illuminate\Support\Str::limit($listing->title, 40) }}</td>
<td class="px-6 py-4 text-green-600 font-medium">{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $listing->status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}">
{{ ucfirst($listing->status) }}
</span>
</td>
<td class="px-6 py-4 text-gray-500 text-sm">{{ $listing->created_at->format('M d, Y') }}</td>
<td class="px-6 py-4">
<a href="{{ route('listings.show', $listing) }}" class="text-blue-600 hover:underline text-sm">View</a>
</td>
</tr>
@empty
<tr><td colspan="5" class="px-6 py-8 text-center text-gray-500">No listings yet. <a href="{{ $partnerCreateRoute }}" class="text-blue-600 hover:underline">Post your first listing!</a></td></tr>
@endforelse
</tbody>
</table>
<div class="px-6 py-4">{{ $myListings->links() }}</div>
</div>
</div>
@endsection

View File

@ -1,32 +0,0 @@
@extends('app::layouts.app')
@section('title', 'My Listings')
@section('content')
@php($partnerCreateRoute = route('filament.partner.resources.listings.create', ['tenant' => auth()->id()]))
<div class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold">My Listings</h1>
<a href="{{ $partnerCreateRoute }}" class="bg-orange-500 text-white px-4 py-2 rounded-lg hover:bg-orange-600 transition">+ New Listing</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@forelse($listings as $listing)
<div class="bg-white rounded-xl shadow-sm overflow-hidden border">
<div class="p-4">
<div class="flex justify-between items-start">
<h3 class="font-semibold text-gray-900 truncate">{{ $listing->title }}</h3>
<span class="ml-2 text-xs px-2 py-1 rounded {{ $listing->status === 'active' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600' }}">{{ ucfirst($listing->status) }}</span>
</div>
<p class="text-green-600 font-bold mt-1">{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}</p>
<p class="text-gray-400 text-xs mt-1">{{ $listing->created_at->format('M d, Y') }}</p>
<a href="{{ route('listings.show', $listing) }}" class="mt-3 block text-center border border-blue-600 text-blue-600 py-1.5 rounded hover:bg-blue-600 hover:text-white transition text-sm">View</a>
</div>
</div>
@empty
<div class="col-span-3 text-center py-12 text-gray-500">
<p>No listings yet.</p>
<a href="{{ $partnerCreateRoute }}" class="mt-3 inline-block bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition">Post Your First Listing</a>
</div>
@endforelse
</div>
<div class="mt-6">{{ $listings->links() }}</div>
</div>
@endsection

View File

@ -7,30 +7,17 @@ use App\Http\Controllers\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\SocialAuthController;
use App\Http\Controllers\Auth\PartnerAuthGatewayController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
Route::get('register', [RegisteredUserController::class, 'create'])
Route::get('register', [PartnerAuthGatewayController::class, 'register'])
->name('register');
Route::post('register', [RegisteredUserController::class, 'store']);
Route::get('login', [AuthenticatedSessionController::class, 'create'])
Route::get('login', [PartnerAuthGatewayController::class, 'login'])
->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']);
Route::get('auth/{provider}/redirect', [SocialAuthController::class, 'redirect'])
->whereIn('provider', ['google', 'facebook', 'apple'])
->name('auth.social.redirect');
Route::get('auth/{provider}/callback', [SocialAuthController::class, 'callback'])
->whereIn('provider', ['google', 'facebook', 'apple'])
->name('auth.social.callback');
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');

View File

@ -2,16 +2,25 @@
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\LanguageController;
use App\Http\Controllers\Partner\DashboardController;
use App\Http\Controllers\Partner\ListingController as PartnerListingController;
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard')->middleware('auth');
Route::get('/lang/{locale}', [LanguageController::class, 'switch'])->name('lang.switch');
Route::middleware('auth')->prefix('partner')->name('partner.')->group(function () {
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
Route::get('/listings', [PartnerListingController::class, 'index'])->name('listings.index');
});
$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'))
->name('dashboard');
Route::get('/partner', fn () => $redirectToPartner('filament.partner.pages.dashboard'))
->name('partner.dashboard');
Route::get('/partner/listings', fn () => $redirectToPartner('filament.partner.resources.listings.index'))
->name('partner.listings.index');
require __DIR__.'/auth.php';