mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
feat: Implement user profile management and favorites module
- Added routes for user profile management including edit, update, and delete functionalities. - Created ProfileController to handle profile-related requests. - Introduced Profile model to manage user profile data. - Developed user status states (Active, Banned, Suspended) with appropriate labels and descriptions. - Implemented favorite listings and sellers functionality in the User model. - Created views for profile editing, updating password, and deleting account. - Added migration for user and profile tables along with necessary fields. - Registered User module with service provider and routes.
This commit is contained in:
parent
72fbabb60b
commit
7e9d77c0a8
@ -1,21 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Modules\Admin\Filament\Resources;
|
namespace Modules\Admin\Filament\Resources;
|
||||||
|
|
||||||
use A909M\FilamentStateFusion\Forms\Components\StateFusionSelect;
|
|
||||||
use A909M\FilamentStateFusion\Tables\Columns\StateFusionSelectColumn;
|
use A909M\FilamentStateFusion\Tables\Columns\StateFusionSelectColumn;
|
||||||
use A909M\FilamentStateFusion\Tables\Filters\StateFusionSelectFilter;
|
use A909M\FilamentStateFusion\Tables\Filters\StateFusionSelectFilter;
|
||||||
use App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\DeleteAction;
|
use Filament\Actions\DeleteAction;
|
||||||
use Filament\Actions\EditAction;
|
use Filament\Actions\EditAction;
|
||||||
use Filament\Forms\Components\Select;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Modules\Admin\Filament\Resources\UserResource\Pages;
|
use Modules\Admin\Filament\Resources\UserResource\Pages;
|
||||||
|
use Modules\User\App\Support\Filament\UserFormFields;
|
||||||
use STS\FilamentImpersonate\Actions\Impersonate;
|
use STS\FilamentImpersonate\Actions\Impersonate;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
@ -28,11 +26,11 @@ class UserResource extends Resource
|
|||||||
public static function form(Schema $schema): Schema
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema->schema([
|
return $schema->schema([
|
||||||
TextInput::make('name')->required()->maxLength(255),
|
UserFormFields::name(),
|
||||||
TextInput::make('email')->email()->required()->maxLength(255)->unique(ignoreRecord: true),
|
UserFormFields::email(),
|
||||||
TextInput::make('password')->password()->required(fn ($livewire) => $livewire instanceof Pages\CreateUser)->dehydrateStateUsing(fn ($state) => filled($state) ? bcrypt($state) : null)->dehydrated(fn ($state) => filled($state)),
|
UserFormFields::password(fn ($livewire) => $livewire instanceof Pages\CreateUser),
|
||||||
StateFusionSelect::make('status')->required(),
|
UserFormFields::status(),
|
||||||
Select::make('roles')->multiple()->relationship('roles', 'name')->preload(),
|
UserFormFields::roles(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Conversation\App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Modules\Conversation\App\Models\Conversation;
|
||||||
|
use Modules\Conversation\App\Support\QuickMessageCatalog;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
|
||||||
|
class ConversationController extends Controller
|
||||||
|
{
|
||||||
|
public function inbox(Request $request): View
|
||||||
|
{
|
||||||
|
$userId = (int) $request->user()->getKey();
|
||||||
|
$messageFilter = $this->resolveMessageFilter($request);
|
||||||
|
|
||||||
|
$conversations = Conversation::inboxForUser($userId, $messageFilter);
|
||||||
|
$selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation'));
|
||||||
|
|
||||||
|
if ($selectedConversation) {
|
||||||
|
$selectedConversation->loadThread();
|
||||||
|
$selectedConversation->markAsReadFor($userId);
|
||||||
|
|
||||||
|
$conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation {
|
||||||
|
if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) {
|
||||||
|
$conversation->unread_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $conversation;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('conversation::inbox', [
|
||||||
|
'conversations' => $conversations,
|
||||||
|
'selectedConversation' => $selectedConversation,
|
||||||
|
'messageFilter' => $messageFilter,
|
||||||
|
'quickMessages' => QuickMessageCatalog::all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function start(Request $request, Listing $listing): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if (! $listing->user_id) {
|
||||||
|
return back()->with('error', 'Bu ilan için mesajlaşma açılamadı.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int) $listing->user_id === (int) $user->getKey()) {
|
||||||
|
return back()->with('error', 'Kendi ilanına mesaj gönderemezsin.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$conversation = Conversation::openForListingBuyer($listing, (int) $user->getKey());
|
||||||
|
|
||||||
|
$user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]);
|
||||||
|
|
||||||
|
$messageBody = trim((string) $request->string('message'));
|
||||||
|
|
||||||
|
if ($messageBody !== '') {
|
||||||
|
$message = $conversation->messages()->create([
|
||||||
|
'sender_id' => $user->getKey(),
|
||||||
|
'body' => $messageBody,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$conversation->forceFill(['last_message_at' => $message->created_at])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()]))
|
||||||
|
->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(Request $request, Conversation $conversation): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
$userId = (int) $user->getKey();
|
||||||
|
|
||||||
|
if ((int) $conversation->buyer_id !== $userId && (int) $conversation->seller_id !== $userId) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $request->validate([
|
||||||
|
'message' => ['required', 'string', 'max:2000'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$messageBody = trim($payload['message']);
|
||||||
|
|
||||||
|
if ($messageBody === '') {
|
||||||
|
return back()->with('error', 'Mesaj boş olamaz.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = $conversation->messages()->create([
|
||||||
|
'sender_id' => $userId,
|
||||||
|
'body' => $messageBody,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$conversation->forceFill(['last_message_at' => $message->created_at])->save();
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('panel.inbox.index', array_merge($this->inboxFilters($request), ['conversation' => $conversation->getKey()]))
|
||||||
|
->with('success', 'Mesaj gönderildi.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function inboxFilters(Request $request): array
|
||||||
|
{
|
||||||
|
$messageFilter = $this->resolveMessageFilter($request);
|
||||||
|
|
||||||
|
return $messageFilter === 'all' ? [] : ['message_filter' => $messageFilter];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveMessageFilter(Request $request): string
|
||||||
|
{
|
||||||
|
$messageFilter = (string) $request->string('message_filter', 'all');
|
||||||
|
|
||||||
|
return in_array($messageFilter, ['all', 'unread', 'important'], true) ? $messageFilter : 'all';
|
||||||
|
}
|
||||||
|
}
|
||||||
168
Modules/Conversation/App/Models/Conversation.php
Normal file
168
Modules/Conversation/App/Models/Conversation.php
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Conversation\App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
use Modules\User\App\Models\User;
|
||||||
|
|
||||||
|
class Conversation extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = ['listing_id', 'seller_id', 'buyer_id', 'last_message_at'];
|
||||||
|
|
||||||
|
protected $casts = ['last_message_at' => 'datetime'];
|
||||||
|
|
||||||
|
public function listing()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Listing::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function seller()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'seller_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buyer()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'buyer_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ConversationMessage::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastMessage()
|
||||||
|
{
|
||||||
|
return $this->hasOne(ConversationMessage::class)
|
||||||
|
->latestOfMany()
|
||||||
|
->select([
|
||||||
|
'conversation_messages.id',
|
||||||
|
'conversation_messages.conversation_id',
|
||||||
|
'conversation_messages.sender_id',
|
||||||
|
'conversation_messages.body',
|
||||||
|
'conversation_messages.created_at',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeForUser(Builder $query, int $userId): Builder
|
||||||
|
{
|
||||||
|
return $query->where(function (Builder $participantQuery) use ($userId): void {
|
||||||
|
$participantQuery->where('buyer_id', $userId)->orWhere('seller_id', $userId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeApplyMessageFilter(Builder $query, int $userId, string $messageFilter): Builder
|
||||||
|
{
|
||||||
|
if (! in_array($messageFilter, ['unread', 'important'], true)) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->whereHas('messages', function (Builder $messageQuery) use ($userId): void {
|
||||||
|
$messageQuery->where('sender_id', '!=', $userId)->whereNull('read_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeWithInboxData(Builder $query, int $userId): Builder
|
||||||
|
{
|
||||||
|
return $query
|
||||||
|
->with([
|
||||||
|
'listing:id,title,price,currency,user_id',
|
||||||
|
'buyer:id,name',
|
||||||
|
'seller:id,name',
|
||||||
|
'lastMessage',
|
||||||
|
'lastMessage.sender:id,name',
|
||||||
|
])
|
||||||
|
->withCount([
|
||||||
|
'messages as unread_count' => fn (Builder $messageQuery) => $messageQuery
|
||||||
|
->where('sender_id', '!=', $userId)
|
||||||
|
->whereNull('read_at'),
|
||||||
|
])
|
||||||
|
->orderByDesc('last_message_at')
|
||||||
|
->orderByDesc('updated_at');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function inboxForUser(int $userId, string $messageFilter = 'all'): EloquentCollection
|
||||||
|
{
|
||||||
|
return static::query()
|
||||||
|
->forUser($userId)
|
||||||
|
->applyMessageFilter($userId, $messageFilter)
|
||||||
|
->withInboxData($userId)
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resolveSelected(EloquentCollection $conversations, ?int $conversationId): ?self
|
||||||
|
{
|
||||||
|
$selectedConversationId = $conversationId;
|
||||||
|
|
||||||
|
if (($selectedConversationId ?? 0) <= 0 && $conversations->isNotEmpty()) {
|
||||||
|
$selectedConversationId = (int) $conversations->first()->getKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($selectedConversationId ?? 0) <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$selected = $conversations->firstWhere('id', $selectedConversationId);
|
||||||
|
|
||||||
|
if (! $selected instanceof self) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadThread(): void
|
||||||
|
{
|
||||||
|
$this->load([
|
||||||
|
'listing:id,title,price,currency,user_id',
|
||||||
|
'messages' => fn (Builder $query) => $query->with('sender:id,name')->orderBy('created_at'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAsReadFor(int $userId): void
|
||||||
|
{
|
||||||
|
ConversationMessage::query()
|
||||||
|
->where('conversation_id', $this->getKey())
|
||||||
|
->where('sender_id', '!=', $userId)
|
||||||
|
->whereNull('read_at')
|
||||||
|
->update([
|
||||||
|
'read_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function openForListingBuyer(Listing $listing, int $buyerId): self
|
||||||
|
{
|
||||||
|
$conversation = static::query()->firstOrCreate(
|
||||||
|
[
|
||||||
|
'listing_id' => $listing->getKey(),
|
||||||
|
'buyer_id' => $buyerId,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'seller_id' => $listing->user_id,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((int) $conversation->seller_id !== (int) $listing->user_id) {
|
||||||
|
$conversation->forceFill(['seller_id' => $listing->user_id])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $conversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function buyerListingConversationId(int $listingId, int $buyerId): ?int
|
||||||
|
{
|
||||||
|
$value = static::query()
|
||||||
|
->where('listing_id', $listingId)
|
||||||
|
->where('buyer_id', $buyerId)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
return is_null($value) ? null : (int) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,24 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Modules\Conversation\App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Modules\User\App\Models\User;
|
||||||
|
|
||||||
class ConversationMessage extends Model
|
class ConversationMessage extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = ['conversation_id', 'sender_id', 'body', 'read_at'];
|
||||||
'conversation_id',
|
|
||||||
'sender_id',
|
|
||||||
'body',
|
|
||||||
'read_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = ['read_at' => 'datetime'];
|
||||||
'read_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function conversation()
|
public function conversation()
|
||||||
{
|
{
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Conversation\App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class ConversationServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->loadMigrationsFrom(module_path('Conversation', 'database/migrations'));
|
||||||
|
$this->loadRoutesFrom(module_path('Conversation', 'routes/web.php'));
|
||||||
|
$this->loadViewsFrom(module_path('Conversation', 'resources/views'), 'conversation');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Modules/Conversation/App/Support/QuickMessageCatalog.php
Normal file
16
Modules/Conversation/App/Support/QuickMessageCatalog.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Conversation\App\Support;
|
||||||
|
|
||||||
|
class QuickMessageCatalog
|
||||||
|
{
|
||||||
|
public static function all(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Merhaba',
|
||||||
|
'İlan hâlâ satışta mı?',
|
||||||
|
'Son fiyat nedir?',
|
||||||
|
'Teşekkürler',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('conversations')) {
|
||||||
|
Schema::create('conversations', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('listing_id')->constrained('listings')->cascadeOnDelete();
|
||||||
|
$table->foreignId('seller_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
$table->foreignId('buyer_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
$table->timestamp('last_message_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['listing_id', 'buyer_id']);
|
||||||
|
$table->index(['seller_id', 'last_message_at']);
|
||||||
|
$table->index(['buyer_id', 'last_message_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('conversation_messages')) {
|
||||||
|
Schema::create('conversation_messages', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('conversation_id')->constrained('conversations')->cascadeOnDelete();
|
||||||
|
$table->foreignId('sender_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
$table->text('body');
|
||||||
|
$table->timestamp('read_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['conversation_id', 'created_at']);
|
||||||
|
$table->index(['conversation_id', 'read_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('conversation_messages');
|
||||||
|
Schema::dropIfExists('conversations');
|
||||||
|
}
|
||||||
|
};
|
||||||
11
Modules/Conversation/module.json
Normal file
11
Modules/Conversation/module.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Conversation",
|
||||||
|
"alias": "conversation",
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"priority": 0,
|
||||||
|
"providers": [
|
||||||
|
"Modules\\Conversation\\App\\Providers\\ConversationServiceProvider"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
13
Modules/Conversation/routes/web.php
Normal file
13
Modules/Conversation/routes/web.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Modules\Conversation\App\Http\Controllers\ConversationController;
|
||||||
|
|
||||||
|
Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
|
||||||
|
Route::get('/gelen-kutusu', [ConversationController::class, 'inbox'])->name('inbox.index');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware('auth')->name('conversations.')->group(function () {
|
||||||
|
Route::post('/listings/{listing}/conversation', [ConversationController::class, 'start'])->name('start');
|
||||||
|
Route::post('/conversations/{conversation}/messages', [ConversationController::class, 'send'])->name('messages.send');
|
||||||
|
});
|
||||||
@ -1,43 +1,42 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace Modules\Favorite\App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Conversation;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\ConversationMessage;
|
|
||||||
use App\Models\FavoriteSearch;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
|
use Modules\Conversation\App\Models\Conversation;
|
||||||
|
use Modules\Conversation\App\Support\QuickMessageCatalog;
|
||||||
|
use Modules\Favorite\App\Models\FavoriteSearch;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
|
use Modules\User\App\Models\User;
|
||||||
|
|
||||||
class FavoriteController extends Controller
|
class FavoriteController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$activeTab = (string) $request->string('tab', 'listings');
|
$activeTab = (string) $request->string('tab', 'listings');
|
||||||
|
|
||||||
if (! in_array($activeTab, ['listings', 'searches', 'sellers'], true)) {
|
if (! in_array($activeTab, ['listings', 'searches', 'sellers'], true)) {
|
||||||
$activeTab = 'listings';
|
$activeTab = 'listings';
|
||||||
}
|
}
|
||||||
|
|
||||||
$statusFilter = (string) $request->string('status', 'all');
|
$statusFilter = (string) $request->string('status', 'all');
|
||||||
|
|
||||||
if (! in_array($statusFilter, ['all', 'active'], true)) {
|
if (! in_array($statusFilter, ['all', 'active'], true)) {
|
||||||
$statusFilter = 'all';
|
$statusFilter = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
$selectedCategoryId = $request->integer('category');
|
|
||||||
|
|
||||||
if ($selectedCategoryId <= 0) {
|
|
||||||
$selectedCategoryId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$messageFilter = (string) $request->string('message_filter', 'all');
|
$messageFilter = (string) $request->string('message_filter', 'all');
|
||||||
if (! in_array($messageFilter, ['all', 'unread', 'important'], true)) {
|
if (! in_array($messageFilter, ['all', 'unread', 'important'], true)) {
|
||||||
$messageFilter = 'all';
|
$messageFilter = 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$selectedCategoryId = $request->integer('category');
|
||||||
|
if ($selectedCategoryId <= 0) {
|
||||||
|
$selectedCategoryId = null;
|
||||||
|
}
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$categories = Category::query()
|
$categories = Category::query()
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
@ -49,12 +48,6 @@ class FavoriteController extends Controller
|
|||||||
$conversations = collect();
|
$conversations = collect();
|
||||||
$selectedConversation = null;
|
$selectedConversation = null;
|
||||||
$buyerConversationListingMap = [];
|
$buyerConversationListingMap = [];
|
||||||
$quickMessages = [
|
|
||||||
'Merhaba',
|
|
||||||
'İlan hâlâ satışta mı?',
|
|
||||||
'Son fiyat nedir?',
|
|
||||||
'Teşekkürler',
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($activeTab === 'listings') {
|
if ($activeTab === 'listings') {
|
||||||
$favoriteListings = $user->favoriteListings()
|
$favoriteListings = $user->favoriteListings()
|
||||||
@ -67,58 +60,18 @@ class FavoriteController extends Controller
|
|||||||
->withQueryString();
|
->withQueryString();
|
||||||
|
|
||||||
$userId = (int) $user->getKey();
|
$userId = (int) $user->getKey();
|
||||||
$conversations = Conversation::query()
|
$conversations = Conversation::inboxForUser($userId, $messageFilter);
|
||||||
->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',
|
|
||||||
'lastMessage.sender:id,name',
|
|
||||||
])
|
|
||||||
->withCount([
|
|
||||||
'messages as unread_count' => fn ($query) => $query
|
|
||||||
->where('sender_id', '!=', $userId)
|
|
||||||
->whereNull('read_at'),
|
|
||||||
])
|
|
||||||
->orderByDesc('last_message_at')
|
|
||||||
->orderByDesc('updated_at')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$buyerConversationListingMap = $conversations
|
$buyerConversationListingMap = $conversations
|
||||||
->where('buyer_id', $userId)
|
->where('buyer_id', $userId)
|
||||||
->pluck('id', 'listing_id')
|
->pluck('id', 'listing_id')
|
||||||
->map(fn ($conversationId) => (int) $conversationId)
|
->map(fn ($conversationId) => (int) $conversationId)
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
$selectedConversationId = $request->integer('conversation');
|
$selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation'));
|
||||||
|
|
||||||
if ($selectedConversationId <= 0 && $conversations->isNotEmpty()) {
|
|
||||||
$selectedConversationId = (int) $conversations->first()->getKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($selectedConversationId > 0) {
|
|
||||||
$selectedConversation = $conversations->firstWhere('id', $selectedConversationId);
|
|
||||||
|
|
||||||
if ($selectedConversation) {
|
if ($selectedConversation) {
|
||||||
$selectedConversation->load([
|
$selectedConversation->loadThread();
|
||||||
'listing:id,title,price,currency,user_id',
|
$selectedConversation->markAsReadFor($userId);
|
||||||
'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 {
|
$conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation {
|
||||||
if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) {
|
if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) {
|
||||||
@ -129,7 +82,6 @@ class FavoriteController extends Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if ($activeTab === 'searches') {
|
if ($activeTab === 'searches') {
|
||||||
$favoriteSearches = $user->favoriteSearches()
|
$favoriteSearches = $user->favoriteSearches()
|
||||||
@ -149,7 +101,7 @@ class FavoriteController extends Controller
|
|||||||
->withQueryString();
|
->withQueryString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('favorites.index', [
|
return view('favorite::index', [
|
||||||
'activeTab' => $activeTab,
|
'activeTab' => $activeTab,
|
||||||
'statusFilter' => $statusFilter,
|
'statusFilter' => $statusFilter,
|
||||||
'selectedCategoryId' => $selectedCategoryId,
|
'selectedCategoryId' => $selectedCategoryId,
|
||||||
@ -161,24 +113,15 @@ class FavoriteController extends Controller
|
|||||||
'conversations' => $conversations,
|
'conversations' => $conversations,
|
||||||
'selectedConversation' => $selectedConversation,
|
'selectedConversation' => $selectedConversation,
|
||||||
'buyerConversationListingMap' => $buyerConversationListingMap,
|
'buyerConversationListingMap' => $buyerConversationListingMap,
|
||||||
'quickMessages' => $quickMessages,
|
'quickMessages' => QuickMessageCatalog::all(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleListing(Request $request, Listing $listing)
|
public function toggleListing(Request $request, Listing $listing)
|
||||||
{
|
{
|
||||||
$user = $request->user();
|
$isNowFavorite = $request->user()->toggleFavoriteListing($listing);
|
||||||
$isFavorite = $user->favoriteListings()->whereKey($listing->getKey())->exists();
|
|
||||||
|
|
||||||
if ($isFavorite) {
|
return back()->with('success', $isNowFavorite ? 'İlan favorilere eklendi.' : 'İlan favorilerden kaldırıldı.');
|
||||||
$user->favoriteListings()->detach($listing->getKey());
|
|
||||||
|
|
||||||
return back()->with('success', 'İlan favorilerden kaldırıldı.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]);
|
|
||||||
|
|
||||||
return back()->with('success', 'İlan favorilere eklendi.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleSeller(Request $request, User $seller)
|
public function toggleSeller(Request $request, User $seller)
|
||||||
@ -189,17 +132,9 @@ class FavoriteController extends Controller
|
|||||||
return back()->with('error', 'Kendi hesabını favorilere ekleyemezsin.');
|
return back()->with('error', 'Kendi hesabını favorilere ekleyemezsin.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$isFavorite = $user->favoriteSellers()->whereKey($seller->getKey())->exists();
|
$isNowFavorite = $user->toggleFavoriteSeller($seller);
|
||||||
|
|
||||||
if ($isFavorite) {
|
return back()->with('success', $isNowFavorite ? 'Satıcı favorilere eklendi.' : 'Satıcı favorilerden kaldırıldı.');
|
||||||
$user->favoriteSellers()->detach($seller->getKey());
|
|
||||||
|
|
||||||
return back()->with('success', 'Satıcı favorilerden kaldırıldı.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->favoriteSellers()->syncWithoutDetaching([$seller->getKey()]);
|
|
||||||
|
|
||||||
return back()->with('success', 'Satıcı favorilere eklendi.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function storeSearch(Request $request)
|
public function storeSearch(Request $request)
|
||||||
@ -225,15 +160,7 @@ class FavoriteController extends Controller
|
|||||||
$categoryName = Category::query()->whereKey($filters['category'])->value('name');
|
$categoryName = Category::query()->whereKey($filters['category'])->value('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
$labelParts = [];
|
$label = FavoriteSearch::labelFor($filters, is_string($categoryName) ? $categoryName : null);
|
||||||
if (! empty($filters['search'])) {
|
|
||||||
$labelParts[] = '"'.$filters['search'].'"';
|
|
||||||
}
|
|
||||||
if ($categoryName) {
|
|
||||||
$labelParts[] = $categoryName;
|
|
||||||
}
|
|
||||||
|
|
||||||
$label = $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtreli arama';
|
|
||||||
|
|
||||||
$favoriteSearch = $request->user()->favoriteSearches()->firstOrCreate(
|
$favoriteSearch = $request->user()->favoriteSearches()->firstOrCreate(
|
||||||
['signature' => $signature],
|
['signature' => $signature],
|
||||||
@ -1,23 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Models;
|
namespace Modules\Favorite\App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Modules\Category\Models\Category;
|
||||||
|
use Modules\User\App\Models\User;
|
||||||
|
|
||||||
class FavoriteSearch extends Model
|
class FavoriteSearch extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = ['user_id', 'label', 'search_term', 'category_id', 'filters', 'signature'];
|
||||||
'user_id',
|
|
||||||
'label',
|
|
||||||
'search_term',
|
|
||||||
'category_id',
|
|
||||||
'filters',
|
|
||||||
'signature',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = ['filters' => 'array'];
|
||||||
'filters' => 'array',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function user()
|
public function user()
|
||||||
{
|
{
|
||||||
@ -26,7 +19,7 @@ class FavoriteSearch extends Model
|
|||||||
|
|
||||||
public function category()
|
public function category()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\Modules\Category\Models\Category::class);
|
return $this->belongsTo(Category::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function normalizeFilters(array $filters): array
|
public static function normalizeFilters(array $filters): array
|
||||||
@ -45,4 +38,19 @@ class FavoriteSearch extends Model
|
|||||||
|
|
||||||
return hash('sha256', is_string($payload) ? $payload : '');
|
return hash('sha256', is_string($payload) ? $payload : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function labelFor(array $filters, ?string $categoryName = null): string
|
||||||
|
{
|
||||||
|
$labelParts = [];
|
||||||
|
|
||||||
|
if (! empty($filters['search'])) {
|
||||||
|
$labelParts[] = '"'.$filters['search'].'"';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filled($categoryName)) {
|
||||||
|
$labelParts[] = $categoryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtreli arama';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
19
Modules/Favorite/App/Providers/FavoriteServiceProvider.php
Normal file
19
Modules/Favorite/App/Providers/FavoriteServiceProvider.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Favorite\App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class FavoriteServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->loadMigrationsFrom(module_path('Favorite', 'database/migrations'));
|
||||||
|
$this->loadRoutesFrom(module_path('Favorite', 'routes/web.php'));
|
||||||
|
$this->loadViewsFrom(module_path('Favorite', 'resources/views'), 'favorite');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('favorite_listings')) {
|
||||||
|
Schema::create('favorite_listings', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('listing_id')->constrained('listings')->cascadeOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['user_id', 'listing_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('favorite_sellers')) {
|
||||||
|
Schema::create('favorite_sellers', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('seller_id')->constrained('users')->cascadeOnDelete();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['user_id', 'seller_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('favorite_searches')) {
|
||||||
|
Schema::create('favorite_searches', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->string('label')->nullable();
|
||||||
|
$table->string('search_term')->nullable();
|
||||||
|
$table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
|
||||||
|
$table->json('filters')->nullable();
|
||||||
|
$table->string('signature', 64);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['user_id', 'signature']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('favorite_searches');
|
||||||
|
Schema::dropIfExists('favorite_sellers');
|
||||||
|
Schema::dropIfExists('favorite_listings');
|
||||||
|
}
|
||||||
|
};
|
||||||
11
Modules/Favorite/module.json
Normal file
11
Modules/Favorite/module.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Favorite",
|
||||||
|
"alias": "favorite",
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"priority": 0,
|
||||||
|
"providers": [
|
||||||
|
"Modules\\Favorite\\App\\Providers\\FavoriteServiceProvider"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
12
Modules/Favorite/routes/web.php
Normal file
12
Modules/Favorite/routes/web.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Modules\Favorite\App\Http\Controllers\FavoriteController;
|
||||||
|
|
||||||
|
Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
|
||||||
|
Route::get('/', [FavoriteController::class, 'index'])->name('index');
|
||||||
|
Route::post('/listings/{listing}/toggle', [FavoriteController::class, 'toggleListing'])->name('listings.toggle');
|
||||||
|
Route::post('/sellers/{seller}/toggle', [FavoriteController::class, 'toggleSeller'])->name('sellers.toggle');
|
||||||
|
Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store');
|
||||||
|
Route::delete('/searches/{favoriteSearch}', [FavoriteController::class, 'destroySearch'])->name('searches.destroy');
|
||||||
|
});
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Modules\Listing\Database\Seeders;
|
namespace Modules\Listing\Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -11,9 +11,6 @@ use Modules\Listing\Models\Listing;
|
|||||||
|
|
||||||
class ListingSeeder extends Seeder
|
class ListingSeeder extends Seeder
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var array<int, array{title: string, description: string, price: int, city: string, country: string, image: string}>
|
|
||||||
*/
|
|
||||||
private const LISTINGS = [
|
private const LISTINGS = [
|
||||||
[
|
[
|
||||||
'title' => 'iPhone 14 Pro 256 GB, temiz kullanılmış',
|
'title' => 'iPhone 14 Pro 256 GB, temiz kullanılmış',
|
||||||
@ -106,9 +103,6 @@ class ListingSeeder extends Seeder
|
|||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array{title: string, description: string, price: int, city: string, country: string, image: string} $data
|
|
||||||
*/
|
|
||||||
private function upsertListing(int $index, array $data, Collection $categories, User $user): Listing
|
private function upsertListing(int $index, array $data, Collection $categories, User $user): Listing
|
||||||
{
|
{
|
||||||
$slug = Str::slug($data['title']) . '-' . ($index + 1);
|
$slug = Str::slug($data['title']) . '-' . ($index + 1);
|
||||||
@ -167,7 +161,11 @@ class ListingSeeder extends Seeder
|
|||||||
|
|
||||||
$media = $mediaItems->first();
|
$media = $mediaItems->first();
|
||||||
|
|
||||||
if (! $media || (string) $media->file_name !== $targetFileName) {
|
if (
|
||||||
|
! $media
|
||||||
|
|| (string) $media->file_name !== $targetFileName
|
||||||
|
|| (string) $media->disk !== 'public'
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
namespace Modules\Listing\Http\Controllers;
|
namespace Modules\Listing\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Conversation;
|
use Modules\Conversation\App\Models\Conversation;
|
||||||
use App\Models\FavoriteSearch;
|
use Modules\Favorite\App\Models\FavoriteSearch;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|||||||
@ -55,18 +55,18 @@ class Listing extends Model implements HasMedia
|
|||||||
|
|
||||||
public function user()
|
public function user()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\App\Models\User::class);
|
return $this->belongsTo(\Modules\User\App\Models\User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function favoritedByUsers()
|
public function favoritedByUsers()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(\App\Models\User::class, 'favorite_listings')
|
return $this->belongsToMany(\Modules\User\App\Models\User::class, 'favorite_listings')
|
||||||
->withTimestamps();
|
->withTimestamps();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function conversations()
|
public function conversations()
|
||||||
{
|
{
|
||||||
return $this->hasMany(\App\Models\Conversation::class);
|
return $this->hasMany(\Modules\Conversation\App\Models\Conversation::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function scopePublicFeed(Builder $query): Builder
|
public function scopePublicFeed(Builder $query): Builder
|
||||||
|
|||||||
@ -20,7 +20,7 @@ use Modules\Listing\Support\ListingPanelHelper;
|
|||||||
use Modules\Location\Models\City;
|
use Modules\Location\Models\City;
|
||||||
use Modules\Location\Models\Country;
|
use Modules\Location\Models\Country;
|
||||||
use Modules\Partner\Filament\Resources\ListingResource;
|
use Modules\Partner\Filament\Resources\ListingResource;
|
||||||
use Modules\Profile\Models\Profile;
|
use Modules\User\App\Models\Profile;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class QuickCreateListing extends Page
|
class QuickCreateListing extends Page
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
namespace Modules\Partner\Providers;
|
namespace Modules\Partner\Providers;
|
||||||
|
|
||||||
use A909M\FilamentStateFusion\FilamentStateFusionPlugin;
|
use A909M\FilamentStateFusion\FilamentStateFusionPlugin;
|
||||||
use App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin;
|
use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin;
|
||||||
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
|
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
|
||||||
use Filament\Http\Middleware\Authenticate;
|
use Filament\Http\Middleware\Authenticate;
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Modules\Profile\Http\Controllers;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Modules\Profile\Models\Profile;
|
|
||||||
|
|
||||||
class ProfileController extends Controller
|
|
||||||
{
|
|
||||||
public function show()
|
|
||||||
{
|
|
||||||
$profile = Profile::firstOrCreate(['user_id' => auth()->id()]);
|
|
||||||
return view('profile::show', compact('profile'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function edit()
|
|
||||||
{
|
|
||||||
$profile = Profile::firstOrCreate(['user_id' => auth()->id()]);
|
|
||||||
return view('profile::edit', compact('profile'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(Request $request)
|
|
||||||
{
|
|
||||||
$data = $request->validate([
|
|
||||||
'bio' => 'nullable|string|max:500',
|
|
||||||
'phone' => 'nullable|string|max:20',
|
|
||||||
'city' => 'nullable|string|max:100',
|
|
||||||
'country' => 'nullable|string|max:100',
|
|
||||||
'website' => 'nullable|url',
|
|
||||||
]);
|
|
||||||
Profile::updateOrCreate(['user_id' => auth()->id()], $data);
|
|
||||||
return redirect()->route('profile.show')->with('success', 'Profile updated!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Modules\Profile\Providers;
|
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
|
||||||
|
|
||||||
class ProfileServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
protected string $moduleName = 'Profile';
|
|
||||||
|
|
||||||
public function boot(): void
|
|
||||||
{
|
|
||||||
$this->loadMigrationsFrom(module_path($this->moduleName, 'database/migrations'));
|
|
||||||
$this->loadRoutesFrom(module_path($this->moduleName, 'routes/web.php'));
|
|
||||||
$this->loadViewsFrom(module_path($this->moduleName, 'resources/views'), 'profile');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function register(): void {}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
<?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::create('profiles', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('user_id')->unique()->constrained('users')->cascadeOnDelete();
|
|
||||||
$table->string('avatar')->nullable();
|
|
||||||
$table->text('bio')->nullable();
|
|
||||||
$table->string('phone')->nullable();
|
|
||||||
$table->string('city')->nullable();
|
|
||||||
$table->string('country')->nullable();
|
|
||||||
$table->string('website')->nullable();
|
|
||||||
$table->boolean('is_verified')->default(false);
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('profiles');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Profile",
|
|
||||||
"alias": "profile",
|
|
||||||
"description": "User profile management",
|
|
||||||
"keywords": [],
|
|
||||||
"priority": 0,
|
|
||||||
"providers": [
|
|
||||||
"Modules\\Profile\\Providers\\ProfileServiceProvider"
|
|
||||||
],
|
|
||||||
"aliases": {},
|
|
||||||
"files": []
|
|
||||||
}
|
|
||||||
@ -1,34 +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">Edit Profile</h1>
|
|
||||||
<form method="POST" action="{{ route('profile.update') }}" class="bg-white rounded-lg shadow-md p-6 space-y-4">
|
|
||||||
@csrf @method('PUT')
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Bio</label>
|
|
||||||
<textarea name="bio" rows="3" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">{{ old('bio', $profile->bio) }}</textarea>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Phone</label>
|
|
||||||
<input type="text" name="phone" value="{{ old('phone', $profile->phone) }}" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">City</label>
|
|
||||||
<input type="text" name="city" value="{{ old('city', $profile->city) }}" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Country</label>
|
|
||||||
<input type="text" name="country" value="{{ old('country', $profile->country) }}" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">Website</label>
|
|
||||||
<input type="url" name="website" value="{{ old('website', $profile->website) }}" class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition font-medium">Save Profile</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endsection
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
@extends('app::layouts.app')
|
|
||||||
@section('content')
|
|
||||||
<div class="container mx-auto px-4 py-8">
|
|
||||||
<div class="max-w-2xl mx-auto bg-white rounded-lg shadow-md p-6">
|
|
||||||
<div class="flex items-center space-x-4 mb-6">
|
|
||||||
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
|
|
||||||
<span class="text-2xl font-bold text-blue-600">{{ substr(auth()->user()->name, 0, 1) }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 class="text-2xl font-bold">{{ auth()->user()->name }}</h1>
|
|
||||||
<p class="text-gray-500">{{ auth()->user()->email }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@if($profile->bio)<p class="text-gray-700 mb-4">{{ $profile->bio }}</p>@endif
|
|
||||||
<div class="space-y-2 text-gray-600">
|
|
||||||
@if($profile->phone)<p>📞 {{ $profile->phone }}</p>@endif
|
|
||||||
@if($profile->city)<p>📍 {{ $profile->city }}@if($profile->country), {{ $profile->country }}@endif</p>@endif
|
|
||||||
@if($profile->website)<p>🌐 <a href="{{ $profile->website }}" class="text-blue-600 hover:underline">{{ $profile->website }}</a></p>@endif
|
|
||||||
</div>
|
|
||||||
<div class="mt-6">
|
|
||||||
<a href="{{ route('profile.edit') }}" class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition">Edit Profile</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endsection
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<?php
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Modules\Profile\Http\Controllers\ProfileController;
|
|
||||||
|
|
||||||
Route::middleware('auth')->prefix('profile')->name('profile.')->group(function () {
|
|
||||||
Route::get('/', [ProfileController::class, 'show'])->name('show');
|
|
||||||
Route::get('/edit', [ProfileController::class, 'edit'])->name('edit');
|
|
||||||
Route::put('/extended', [ProfileController::class, 'update'])->name('update');
|
|
||||||
});
|
|
||||||
@ -1,23 +1,33 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
namespace Modules\User\App\Http\Controllers;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class ProfileController extends Controller
|
class ProfileController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
public function edit(Request $request): View
|
||||||
* Update the user's profile information.
|
{
|
||||||
*/
|
return view('user::profile.edit', ['user' => $request->user()]);
|
||||||
|
}
|
||||||
|
|
||||||
public function update(Request $request): RedirectResponse
|
public function update(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validateWithBag('updateProfile', [
|
$validated = $request->validateWithBag('updateProfile', [
|
||||||
'name' => ['required', 'string', 'max:255'],
|
'name' => ['required', 'string', 'max:255'],
|
||||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique('users')->ignore($request->user()->id)],
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'lowercase',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('users')->ignore($request->user()->id),
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$request->user()->fill($validated);
|
$request->user()->fill($validated);
|
||||||
@ -28,12 +38,9 @@ class ProfileController extends Controller
|
|||||||
|
|
||||||
$request->user()->save();
|
$request->user()->save();
|
||||||
|
|
||||||
return redirect('/profile')->with('status', 'profile-updated');
|
return redirect()->route('profile.edit')->with('status', 'profile-updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the user's account.
|
|
||||||
*/
|
|
||||||
public function destroy(Request $request): RedirectResponse
|
public function destroy(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validateWithBag('userDeletion', [
|
$request->validateWithBag('userDeletion', [
|
||||||
@ -43,7 +50,6 @@ class ProfileController extends Controller
|
|||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
Auth::logout();
|
Auth::logout();
|
||||||
|
|
||||||
$user->delete();
|
$user->delete();
|
||||||
|
|
||||||
$request->session()->invalidate();
|
$request->session()->invalidate();
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Modules\Profile\Models;
|
|
||||||
|
namespace Modules\User\App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Spatie\Activitylog\LogOptions;
|
use Spatie\Activitylog\LogOptions;
|
||||||
@ -10,6 +11,7 @@ class Profile extends Model
|
|||||||
use LogsActivity;
|
use LogsActivity;
|
||||||
|
|
||||||
protected $fillable = ['user_id', 'avatar', 'bio', 'phone', 'city', 'country', 'website', 'is_verified'];
|
protected $fillable = ['user_id', 'avatar', 'bio', 'phone', 'city', 'country', 'website', 'is_verified'];
|
||||||
|
|
||||||
protected $casts = ['is_verified' => 'boolean'];
|
protected $casts = ['is_verified' => 'boolean'];
|
||||||
|
|
||||||
public function getActivitylogOptions(): LogOptions
|
public function getActivitylogOptions(): LogOptions
|
||||||
@ -22,6 +24,6 @@ class Profile extends Model
|
|||||||
|
|
||||||
public function user()
|
public function user()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\App\Models\User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,11 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use App\States\UserStatus;
|
namespace Modules\User\App\Models;
|
||||||
use Filament\Models\Contracts\HasAvatar;
|
|
||||||
use Filament\Models\Contracts\FilamentUser;
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
|
use Filament\Models\Contracts\HasAvatar;
|
||||||
use Filament\Models\Contracts\HasTenants;
|
use Filament\Models\Contracts\HasTenants;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
@ -14,6 +15,11 @@ use Illuminate\Support\Collection;
|
|||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Jeffgreco13\FilamentBreezy\Traits\TwoFactorAuthenticatable;
|
use Jeffgreco13\FilamentBreezy\Traits\TwoFactorAuthenticatable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
use Modules\Conversation\App\Models\Conversation;
|
||||||
|
use Modules\Conversation\App\Models\ConversationMessage;
|
||||||
|
use Modules\Favorite\App\Models\FavoriteSearch;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
use Modules\User\App\States\UserStatus;
|
||||||
use Spatie\Activitylog\LogOptions;
|
use Spatie\Activitylog\LogOptions;
|
||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
use Spatie\ModelStates\HasStates;
|
use Spatie\ModelStates\HasStates;
|
||||||
@ -21,9 +27,16 @@ use Spatie\Permission\Traits\HasRoles;
|
|||||||
|
|
||||||
class User extends Authenticatable implements FilamentUser, HasTenants, HasAvatar
|
class User extends Authenticatable implements FilamentUser, HasTenants, HasAvatar
|
||||||
{
|
{
|
||||||
use HasApiTokens, HasFactory, HasRoles, LogsActivity, Notifiable, HasStates, TwoFactorAuthenticatable;
|
use HasApiTokens;
|
||||||
|
use HasFactory;
|
||||||
|
use HasRoles;
|
||||||
|
use LogsActivity;
|
||||||
|
use Notifiable;
|
||||||
|
use HasStates;
|
||||||
|
use TwoFactorAuthenticatable;
|
||||||
|
|
||||||
protected $fillable = ['name', 'email', 'password', 'avatar_url', 'status'];
|
protected $fillable = ['name', 'email', 'password', 'avatar_url', 'status'];
|
||||||
|
|
||||||
protected $hidden = ['password', 'remember_token'];
|
protected $hidden = ['password', 'remember_token'];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
@ -35,6 +48,11 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function newFactory(): Factory
|
||||||
|
{
|
||||||
|
return \Database\Factories\UserFactory::new();
|
||||||
|
}
|
||||||
|
|
||||||
public function getActivitylogOptions(): LogOptions
|
public function getActivitylogOptions(): LogOptions
|
||||||
{
|
{
|
||||||
return LogOptions::defaults()
|
return LogOptions::defaults()
|
||||||
@ -64,19 +82,22 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata
|
|||||||
|
|
||||||
public function listings()
|
public function listings()
|
||||||
{
|
{
|
||||||
return $this->hasMany(\Modules\Listing\Models\Listing::class);
|
return $this->hasMany(Listing::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Profile::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function favoriteListings()
|
public function favoriteListings()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(\Modules\Listing\Models\Listing::class, 'favorite_listings')
|
return $this->belongsToMany(Listing::class, 'favorite_listings')->withTimestamps();
|
||||||
->withTimestamps();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function favoriteSellers()
|
public function favoriteSellers()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(self::class, 'favorite_sellers', 'user_id', 'seller_id')
|
return $this->belongsToMany(self::class, 'favorite_sellers', 'user_id', 'seller_id')->withTimestamps();
|
||||||
->withTimestamps();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function favoriteSearches()
|
public function favoriteSearches()
|
||||||
@ -111,8 +132,40 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata
|
|||||||
|
|
||||||
public function getFilamentAvatarUrl(): ?string
|
public function getFilamentAvatarUrl(): ?string
|
||||||
{
|
{
|
||||||
return filled($this->avatar_url)
|
return filled($this->avatar_url) ? Storage::disk('public')->url($this->avatar_url) : null;
|
||||||
? Storage::disk('public')->url($this->avatar_url)
|
}
|
||||||
: null;
|
|
||||||
|
public function toggleFavoriteListing(Listing $listing): bool
|
||||||
|
{
|
||||||
|
$isFavorite = $this->favoriteListings()->whereKey($listing->getKey())->exists();
|
||||||
|
|
||||||
|
if ($isFavorite) {
|
||||||
|
$this->favoriteListings()->detach($listing->getKey());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->favoriteListings()->syncWithoutDetaching([$listing->getKey()]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleFavoriteSeller(self $seller): bool
|
||||||
|
{
|
||||||
|
if ((int) $seller->getKey() === (int) $this->getKey()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isFavorite = $this->favoriteSellers()->whereKey($seller->getKey())->exists();
|
||||||
|
|
||||||
|
if ($isFavorite) {
|
||||||
|
$this->favoriteSellers()->detach($seller->getKey());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->favoriteSellers()->syncWithoutDetaching([$seller->getKey()]);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
19
Modules/User/App/Providers/UserServiceProvider.php
Normal file
19
Modules/User/App/Providers/UserServiceProvider.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\User\App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class UserServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
$this->loadMigrationsFrom(module_path('User', 'database/migrations'));
|
||||||
|
$this->loadRoutesFrom(module_path('User', 'routes/web.php'));
|
||||||
|
$this->loadViewsFrom(module_path('User', 'resources/views'), 'user');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\States;
|
namespace Modules\User\App\States;
|
||||||
|
|
||||||
use Filament\Support\Contracts\HasColor;
|
use Filament\Support\Contracts\HasColor;
|
||||||
use Filament\Support\Contracts\HasDescription;
|
use Filament\Support\Contracts\HasDescription;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\States;
|
namespace Modules\User\App\States;
|
||||||
|
|
||||||
use Filament\Support\Contracts\HasColor;
|
use Filament\Support\Contracts\HasColor;
|
||||||
use Filament\Support\Contracts\HasDescription;
|
use Filament\Support\Contracts\HasDescription;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\States;
|
namespace Modules\User\App\States;
|
||||||
|
|
||||||
use Filament\Support\Contracts\HasColor;
|
use Filament\Support\Contracts\HasColor;
|
||||||
use Filament\Support\Contracts\HasDescription;
|
use Filament\Support\Contracts\HasDescription;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\States;
|
namespace Modules\User\App\States;
|
||||||
|
|
||||||
use A909M\FilamentStateFusion\Concerns\StateFusionInfo;
|
use A909M\FilamentStateFusion\Concerns\StateFusionInfo;
|
||||||
use A909M\FilamentStateFusion\Contracts\HasFilamentStateFusion;
|
use A909M\FilamentStateFusion\Contracts\HasFilamentStateFusion;
|
||||||
39
Modules/User/App/Support/Filament/UserFormFields.php
Normal file
39
Modules/User/App/Support/Filament/UserFormFields.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\User\App\Support\Filament;
|
||||||
|
|
||||||
|
use A909M\FilamentStateFusion\Forms\Components\StateFusionSelect;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
|
||||||
|
class UserFormFields
|
||||||
|
{
|
||||||
|
public static function name(): TextInput
|
||||||
|
{
|
||||||
|
return TextInput::make('name')->required()->maxLength(255);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function email(): TextInput
|
||||||
|
{
|
||||||
|
return TextInput::make('email')->email()->required()->maxLength(255)->unique(ignoreRecord: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function password(callable $requiredCallback): TextInput
|
||||||
|
{
|
||||||
|
return TextInput::make('password')
|
||||||
|
->password()
|
||||||
|
->required($requiredCallback)
|
||||||
|
->dehydrateStateUsing(fn ($state) => filled($state) ? bcrypt($state) : null)
|
||||||
|
->dehydrated(fn ($state) => filled($state));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function status(): StateFusionSelect
|
||||||
|
{
|
||||||
|
return StateFusionSelect::make('status')->required();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function roles(): Select
|
||||||
|
{
|
||||||
|
return Select::make('roles')->multiple()->relationship('roles', 'name')->preload();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('users')) {
|
||||||
|
Schema::create('users', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('email')->unique();
|
||||||
|
$table->timestamp('email_verified_at')->nullable();
|
||||||
|
$table->string('status')->default('active');
|
||||||
|
$table->string('password');
|
||||||
|
$table->string('avatar_url')->nullable();
|
||||||
|
$table->rememberToken();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('profiles')) {
|
||||||
|
Schema::create('profiles', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('user_id')->unique()->constrained('users')->cascadeOnDelete();
|
||||||
|
$table->string('avatar')->nullable();
|
||||||
|
$table->text('bio')->nullable();
|
||||||
|
$table->string('phone')->nullable();
|
||||||
|
$table->string('city')->nullable();
|
||||||
|
$table->string('country')->nullable();
|
||||||
|
$table->string('website')->nullable();
|
||||||
|
$table->boolean('is_verified')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('password_reset_tokens')) {
|
||||||
|
Schema::create('password_reset_tokens', function (Blueprint $table): void {
|
||||||
|
$table->string('email')->primary();
|
||||||
|
$table->string('token');
|
||||||
|
$table->timestamp('created_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('sessions')) {
|
||||||
|
Schema::create('sessions', function (Blueprint $table): void {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->longText('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
Schema::dropIfExists('password_reset_tokens');
|
||||||
|
Schema::dropIfExists('profiles');
|
||||||
|
Schema::dropIfExists('users');
|
||||||
|
}
|
||||||
|
};
|
||||||
11
Modules/User/module.json
Normal file
11
Modules/User/module.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "User",
|
||||||
|
"alias": "user",
|
||||||
|
"description": "",
|
||||||
|
"keywords": [],
|
||||||
|
"priority": 0,
|
||||||
|
"providers": [
|
||||||
|
"Modules\\User\\App\\Providers\\UserServiceProvider"
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
@ -9,19 +9,19 @@
|
|||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||||
<div class="max-w-xl">
|
<div class="max-w-xl">
|
||||||
@include('profile.partials.update-profile-information-form')
|
@include('user::profile.partials.update-profile-information-form')
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||||
<div class="max-w-xl">
|
<div class="max-w-xl">
|
||||||
@include('profile.partials.update-password-form')
|
@include('user::profile.partials.update-password-form')
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||||
<div class="max-w-xl">
|
<div class="max-w-xl">
|
||||||
@include('profile.partials.delete-user-form')
|
@include('user::profile.partials.delete-user-form')
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
10
Modules/User/routes/web.php
Normal file
10
Modules/User/routes/web.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Modules\User\App\Http\Controllers\ProfileController;
|
||||||
|
|
||||||
|
Route::middleware('auth')->prefix('profile')->name('profile.')->group(function () {
|
||||||
|
Route::get('/', [ProfileController::class, 'edit'])->name('edit');
|
||||||
|
Route::patch('/', [ProfileController::class, 'update'])->name('update');
|
||||||
|
Route::delete('/', [ProfileController::class, 'destroy'])->name('destroy');
|
||||||
|
});
|
||||||
@ -3,7 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
use Illuminate\Auth\Events\PasswordReset;
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -15,19 +15,11 @@ use Illuminate\View\View;
|
|||||||
|
|
||||||
class NewPasswordController extends Controller
|
class NewPasswordController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Display the password reset view.
|
|
||||||
*/
|
|
||||||
public function create(Request $request): View
|
public function create(Request $request): View
|
||||||
{
|
{
|
||||||
return view('auth.reset-password', ['request' => $request]);
|
return view('auth.reset-password', ['request' => $request]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an incoming new password request.
|
|
||||||
*
|
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
|
||||||
*/
|
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
@ -36,9 +28,6 @@ class NewPasswordController extends Controller
|
|||||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Here we will attempt to reset the user's password. If it is successful we
|
|
||||||
// will update the password on an actual user model and persist it to the
|
|
||||||
// database. Otherwise we will parse the error and return the response.
|
|
||||||
$status = Password::reset(
|
$status = Password::reset(
|
||||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||||
function (User $user) use ($request) {
|
function (User $user) use ($request) {
|
||||||
@ -51,9 +40,6 @@ class NewPasswordController extends Controller
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// If the password was successfully reset, we will redirect the user back to
|
|
||||||
// the application's home authenticated view. If there is an error we can
|
|
||||||
// redirect them back to where they came from with their error message.
|
|
||||||
return $status == Password::PASSWORD_RESET
|
return $status == Password::PASSWORD_RESET
|
||||||
? redirect()->route('login')->with('status', __($status))
|
? redirect()->route('login')->with('status', __($status))
|
||||||
: back()->withInput($request->only('email'))
|
: back()->withInput($request->only('email'))
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
use Illuminate\Auth\Events\Registered;
|
use Illuminate\Auth\Events\Registered;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -14,19 +14,11 @@ use Illuminate\View\View;
|
|||||||
|
|
||||||
class RegisteredUserController extends Controller
|
class RegisteredUserController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Display the registration view.
|
|
||||||
*/
|
|
||||||
public function create(): View
|
public function create(): View
|
||||||
{
|
{
|
||||||
return view('auth.register');
|
return view('auth.register');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an incoming registration request.
|
|
||||||
*
|
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
|
||||||
*/
|
|
||||||
public function store(Request $request): RedirectResponse
|
public function store(Request $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$request->validate([
|
$request->validate([
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
@ -15,9 +15,6 @@ use Throwable;
|
|||||||
|
|
||||||
class SocialAuthController extends Controller
|
class SocialAuthController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var array<int, string>
|
|
||||||
*/
|
|
||||||
private array $allowedProviders = ['google', 'facebook', 'apple'];
|
private array $allowedProviders = ['google', 'facebook', 'apple'];
|
||||||
|
|
||||||
public function redirect(string $provider): RedirectResponse
|
public function redirect(string $provider): RedirectResponse
|
||||||
|
|||||||
@ -1,104 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\Conversation;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Modules\Listing\Models\Listing;
|
|
||||||
|
|
||||||
class ConversationController extends Controller
|
|
||||||
{
|
|
||||||
public function start(Request $request, Listing $listing): RedirectResponse
|
|
||||||
{
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
if (! $listing->user_id) {
|
|
||||||
return back()->with('error', 'Bu ilan için mesajlaşma açılamadı.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((int) $listing->user_id === (int) $user->getKey()) {
|
|
||||||
return back()->with('error', 'Kendi ilanına mesaj gönderemezsin.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$conversation = Conversation::query()->firstOrCreate(
|
|
||||||
[
|
|
||||||
'listing_id' => $listing->getKey(),
|
|
||||||
'buyer_id' => $user->getKey(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'seller_id' => $listing->user_id,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if ((int) $conversation->seller_id !== (int) $listing->user_id) {
|
|
||||||
$conversation->forceFill([
|
|
||||||
'seller_id' => $listing->user_id,
|
|
||||||
])->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->favoriteListings()->syncWithoutDetaching([$listing->getKey()]);
|
|
||||||
|
|
||||||
$messageBody = trim((string) $request->string('message'));
|
|
||||||
|
|
||||||
if ($messageBody !== '') {
|
|
||||||
$message = $conversation->messages()->create([
|
|
||||||
'sender_id' => $user->getKey(),
|
|
||||||
'body' => $messageBody,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$conversation->forceFill([
|
|
||||||
'last_message_at' => $message->created_at,
|
|
||||||
])->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()
|
|
||||||
->route('panel.inbox.index', array_merge(
|
|
||||||
$this->inboxFilters($request),
|
|
||||||
['conversation' => $conversation->getKey()],
|
|
||||||
))
|
|
||||||
->with('success', $messageBody !== '' ? 'Mesaj gönderildi.' : 'Sohbet açıldı.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function send(Request $request, Conversation $conversation): RedirectResponse
|
|
||||||
{
|
|
||||||
$user = $request->user();
|
|
||||||
$userId = (int) $user->getKey();
|
|
||||||
|
|
||||||
if ((int) $conversation->buyer_id !== $userId && (int) $conversation->seller_id !== $userId) {
|
|
||||||
abort(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$payload = $request->validate([
|
|
||||||
'message' => ['required', 'string', 'max:2000'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$message = $conversation->messages()->create([
|
|
||||||
'sender_id' => $userId,
|
|
||||||
'body' => trim($payload['message']),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$conversation->forceFill([
|
|
||||||
'last_message_at' => $message->created_at,
|
|
||||||
])->save();
|
|
||||||
|
|
||||||
return redirect()
|
|
||||||
->route('panel.inbox.index', array_merge(
|
|
||||||
$this->inboxFilters($request),
|
|
||||||
['conversation' => $conversation->getKey()],
|
|
||||||
))
|
|
||||||
->with('success', 'Mesaj gönderildi.');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function inboxFilters(Request $request): array
|
|
||||||
{
|
|
||||||
$filters = [];
|
|
||||||
|
|
||||||
$messageFilter = (string) $request->string('message_filter');
|
|
||||||
if (in_array($messageFilter, ['all', 'unread', 'important'], true)) {
|
|
||||||
$filters['message_filter'] = $messageFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $filters;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@ namespace App\Http\Controllers;
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
use App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
|
|
||||||
class HomeController extends Controller
|
class HomeController extends Controller
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Conversation;
|
|
||||||
use App\Models\ConversationMessage;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
@ -59,88 +57,6 @@ class PanelController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
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',
|
|
||||||
])
|
|
||||||
->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
|
public function destroyListing(Request $request, Listing $listing): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->guardListingOwner($request, $listing);
|
$this->guardListingOwner($request, $listing);
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Requests\ProfileUpdateRequest;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Redirect;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class ProfileController extends Controller
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Display the user's profile form.
|
|
||||||
*/
|
|
||||||
public function edit(Request $request): View
|
|
||||||
{
|
|
||||||
return view('profile.edit', [
|
|
||||||
'user' => $request->user(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the user's profile information.
|
|
||||||
*/
|
|
||||||
public function update(ProfileUpdateRequest $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$request->user()->fill($request->validated());
|
|
||||||
|
|
||||||
if ($request->user()->isDirty('email')) {
|
|
||||||
$request->user()->email_verified_at = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$request->user()->save();
|
|
||||||
|
|
||||||
return Redirect::route('profile.edit')->with('status', 'profile-updated');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the user's account.
|
|
||||||
*/
|
|
||||||
public function destroy(Request $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$request->validateWithBag('userDeletion', [
|
|
||||||
'password' => ['required', 'current_password'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
Auth::logout();
|
|
||||||
|
|
||||||
$user->delete();
|
|
||||||
|
|
||||||
$request->session()->invalidate();
|
|
||||||
$request->session()->regenerateToken();
|
|
||||||
|
|
||||||
return Redirect::to('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class ProfileUpdateRequest extends FormRequest
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'name' => ['required', 'string', 'max:255'],
|
|
||||||
'email' => [
|
|
||||||
'required',
|
|
||||||
'string',
|
|
||||||
'lowercase',
|
|
||||||
'email',
|
|
||||||
'max:255',
|
|
||||||
Rule::unique(User::class)->ignore($this->user()->id),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -16,7 +16,7 @@ use Modules\Listing\Support\ListingCustomFieldSchemaBuilder;
|
|||||||
use Modules\Listing\Support\ListingPanelHelper;
|
use Modules\Listing\Support\ListingPanelHelper;
|
||||||
use Modules\Location\Models\City;
|
use Modules\Location\Models\City;
|
||||||
use Modules\Location\Models\Country;
|
use Modules\Location\Models\Country;
|
||||||
use Modules\Profile\Models\Profile;
|
use Modules\User\App\Models\Profile;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class PanelQuickListingForm extends Component
|
class PanelQuickListingForm extends Component
|
||||||
|
|||||||
@ -1,76 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Modules\Listing\Models\Listing;
|
|
||||||
|
|
||||||
class Conversation extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'listing_id',
|
|
||||||
'seller_id',
|
|
||||||
'buyer_id',
|
|
||||||
'last_message_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'last_message_at' => 'datetime',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function listing()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Listing::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function seller()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'seller_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buyer()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class, 'buyer_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function messages()
|
|
||||||
{
|
|
||||||
return $this->hasMany(ConversationMessage::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function lastMessage()
|
|
||||||
{
|
|
||||||
return $this->hasOne(ConversationMessage::class)
|
|
||||||
->latestOfMany()
|
|
||||||
->select([
|
|
||||||
'conversation_messages.id',
|
|
||||||
'conversation_messages.conversation_id',
|
|
||||||
'conversation_messages.sender_id',
|
|
||||||
'conversation_messages.body',
|
|
||||||
'conversation_messages.created_at',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function scopeForUser(Builder $query, int $userId): Builder
|
|
||||||
{
|
|
||||||
return $query->where(function (Builder $participantQuery) use ($userId): void {
|
|
||||||
$participantQuery
|
|
||||||
->where('buyer_id', $userId)
|
|
||||||
->orWhere('seller_id', $userId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function buyerListingConversationId(int $listingId, int $buyerId): ?int
|
|
||||||
{
|
|
||||||
$value = static::query()
|
|
||||||
->where('listing_id', $listingId)
|
|
||||||
->where('buyer_id', $buyerId)
|
|
||||||
->value('id');
|
|
||||||
|
|
||||||
return is_null($value) ? null : (int) $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -62,7 +62,7 @@ return [
|
|||||||
'providers' => [
|
'providers' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
'model' => env('AUTH_MODEL', Modules\User\App\Models\User::class),
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// 'users' => [
|
||||||
|
|||||||
@ -5,22 +5,14 @@ namespace Database\Factories;
|
|||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Modules\User\App\Models\User;
|
||||||
|
|
||||||
/**
|
|
||||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
|
||||||
*/
|
|
||||||
class UserFactory extends Factory
|
class UserFactory extends Factory
|
||||||
{
|
{
|
||||||
/**
|
protected $model = User::class;
|
||||||
* The current password being used by the factory.
|
|
||||||
*/
|
|
||||||
protected static ?string $password;
|
protected static ?string $password;
|
||||||
|
|
||||||
/**
|
|
||||||
* Define the model's default state.
|
|
||||||
*
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -32,9 +24,6 @@ class UserFactory extends Factory
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicate that the model's email address should be unverified.
|
|
||||||
*/
|
|
||||||
public function unverified(): static
|
public function unverified(): static
|
||||||
{
|
{
|
||||||
return $this->state(fn (array $attributes) => [
|
return $this->state(fn (array $attributes) => [
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('users', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('name');
|
|
||||||
$table->string('email')->unique();
|
|
||||||
$table->timestamp('email_verified_at')->nullable();
|
|
||||||
$table->string('password');
|
|
||||||
$table->rememberToken();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
|
||||||
$table->string('email')->primary();
|
|
||||||
$table->string('token');
|
|
||||||
$table->timestamp('created_at')->nullable();
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('sessions', function (Blueprint $table) {
|
|
||||||
$table->string('id')->primary();
|
|
||||||
$table->foreignId('user_id')->nullable()->index();
|
|
||||||
$table->string('ip_address', 45)->nullable();
|
|
||||||
$table->text('user_agent')->nullable();
|
|
||||||
$table->longText('payload');
|
|
||||||
$table->integer('last_activity')->index();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('users');
|
|
||||||
Schema::dropIfExists('password_reset_tokens');
|
|
||||||
Schema::dropIfExists('sessions');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<?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('users', function (Blueprint $table): void {
|
|
||||||
if (! Schema::hasColumn('users', 'avatar_url')) {
|
|
||||||
$table->string('avatar_url')->nullable()->after('password');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! Schema::hasColumn('users', 'status')) {
|
|
||||||
$table->string('status')->default('active')->after('email_verified_at');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('users', function (Blueprint $table): void {
|
|
||||||
if (Schema::hasColumn('users', 'avatar_url')) {
|
|
||||||
$table->dropColumn('avatar_url');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Schema::hasColumn('users', 'status')) {
|
|
||||||
$table->dropColumn('status');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
<?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::create('favorite_listings', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->foreignId('listing_id')->constrained('listings')->cascadeOnDelete();
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->unique(['user_id', 'listing_id']);
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('favorite_sellers', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->foreignId('seller_id')->constrained('users')->cascadeOnDelete();
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->unique(['user_id', 'seller_id']);
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('favorite_searches', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
|
||||||
$table->string('label')->nullable();
|
|
||||||
$table->string('search_term')->nullable();
|
|
||||||
$table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
|
|
||||||
$table->json('filters')->nullable();
|
|
||||||
$table->string('signature', 64);
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->unique(['user_id', 'signature']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('favorite_searches');
|
|
||||||
Schema::dropIfExists('favorite_sellers');
|
|
||||||
Schema::dropIfExists('favorite_listings');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
<?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::create('conversations', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('listing_id')->constrained('listings')->cascadeOnDelete();
|
|
||||||
$table->foreignId('seller_id')->constrained('users')->cascadeOnDelete();
|
|
||||||
$table->foreignId('buyer_id')->constrained('users')->cascadeOnDelete();
|
|
||||||
$table->timestamp('last_message_at')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->unique(['listing_id', 'buyer_id']);
|
|
||||||
$table->index(['seller_id', 'last_message_at']);
|
|
||||||
$table->index(['buyer_id', 'last_message_at']);
|
|
||||||
});
|
|
||||||
|
|
||||||
Schema::create('conversation_messages', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('conversation_id')->constrained('conversations')->cascadeOnDelete();
|
|
||||||
$table->foreignId('sender_id')->constrained('users')->cascadeOnDelete();
|
|
||||||
$table->text('body');
|
|
||||||
$table->timestamp('read_at')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->index(['conversation_id', 'created_at']);
|
|
||||||
$table->index(['conversation_id', 'read_at']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('conversation_messages');
|
|
||||||
Schema::dropIfExists('conversations');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Role;
|
||||||
|
|||||||
@ -2,8 +2,10 @@
|
|||||||
"Category": true,
|
"Category": true,
|
||||||
"Listing": true,
|
"Listing": true,
|
||||||
"Location": true,
|
"Location": true,
|
||||||
"Profile": true,
|
|
||||||
"Admin": true,
|
"Admin": true,
|
||||||
"Partner": false,
|
"Partner": false,
|
||||||
"Theme": true
|
"Theme": true,
|
||||||
|
"Conversation": true,
|
||||||
|
"Favorite": true,
|
||||||
|
"User": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,9 +52,6 @@ Route::middleware('auth')->group(function () {
|
|||||||
|
|
||||||
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
|
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
|
||||||
|
|
||||||
Route::patch('profile', [\App\Http\Controllers\Auth\ProfileController::class, 'update'])->name('profile.update');
|
|
||||||
Route::delete('profile', [\App\Http\Controllers\Auth\ProfileController::class, 'destroy'])->name('profile.destroy');
|
|
||||||
|
|
||||||
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
|
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
|
||||||
->name('logout');
|
->name('logout');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\ConversationController;
|
|
||||||
use App\Http\Controllers\FavoriteController;
|
|
||||||
use App\Http\Controllers\HomeController;
|
use App\Http\Controllers\HomeController;
|
||||||
use App\Http\Controllers\LanguageController;
|
use App\Http\Controllers\LanguageController;
|
||||||
use App\Http\Controllers\PanelController;
|
use App\Http\Controllers\PanelController;
|
||||||
@ -18,7 +16,6 @@ Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
|
|||||||
Route::get('/', [PanelController::class, 'index'])->name('index');
|
Route::get('/', [PanelController::class, 'index'])->name('index');
|
||||||
Route::get('/ilanlarim', [PanelController::class, 'listings'])->name('listings.index');
|
Route::get('/ilanlarim', [PanelController::class, 'listings'])->name('listings.index');
|
||||||
Route::get('/ilan-ver', [PanelController::class, 'create'])->name('listings.create');
|
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}/kaldir', [PanelController::class, 'destroyListing'])->name('listings.destroy');
|
||||||
Route::post('/ilanlarim/{listing}/satildi', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold');
|
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::post('/ilanlarim/{listing}/yeniden-yayinla', [PanelController::class, 'republishListing'])->name('listings.republish');
|
||||||
@ -27,17 +24,4 @@ Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
|
|||||||
Route::get('/partner/{any?}', fn () => redirect()->route('panel.listings.index'))
|
Route::get('/partner/{any?}', fn () => redirect()->route('panel.listings.index'))
|
||||||
->where('any', '.*');
|
->where('any', '.*');
|
||||||
|
|
||||||
Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
|
|
||||||
Route::get('/', [FavoriteController::class, 'index'])->name('index');
|
|
||||||
Route::post('/listings/{listing}/toggle', [FavoriteController::class, 'toggleListing'])->name('listings.toggle');
|
|
||||||
Route::post('/sellers/{seller}/toggle', [FavoriteController::class, 'toggleSeller'])->name('sellers.toggle');
|
|
||||||
Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store');
|
|
||||||
Route::delete('/searches/{favoriteSearch}', [FavoriteController::class, 'destroySearch'])->name('searches.destroy');
|
|
||||||
});
|
|
||||||
|
|
||||||
Route::middleware('auth')->name('conversations.')->group(function () {
|
|
||||||
Route::post('/listings/{listing}/conversation', [ConversationController::class, 'start'])->name('start');
|
|
||||||
Route::post('/conversations/{conversation}/messages', [ConversationController::class, 'send'])->name('messages.send');
|
|
||||||
});
|
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user