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
|
||||
namespace Modules\Admin\Filament\Resources;
|
||||
|
||||
use A909M\FilamentStateFusion\Forms\Components\StateFusionSelect;
|
||||
use A909M\FilamentStateFusion\Tables\Columns\StateFusionSelectColumn;
|
||||
use A909M\FilamentStateFusion\Tables\Filters\StateFusionSelectFilter;
|
||||
use App\Models\User;
|
||||
use Modules\User\App\Models\User;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Modules\Admin\Filament\Resources\UserResource\Pages;
|
||||
use Modules\User\App\Support\Filament\UserFormFields;
|
||||
use STS\FilamentImpersonate\Actions\Impersonate;
|
||||
use UnitEnum;
|
||||
|
||||
@ -28,11 +26,11 @@ class UserResource extends Resource
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema->schema([
|
||||
TextInput::make('name')->required()->maxLength(255),
|
||||
TextInput::make('email')->email()->required()->maxLength(255)->unique(ignoreRecord: true),
|
||||
TextInput::make('password')->password()->required(fn ($livewire) => $livewire instanceof Pages\CreateUser)->dehydrateStateUsing(fn ($state) => filled($state) ? bcrypt($state) : null)->dehydrated(fn ($state) => filled($state)),
|
||||
StateFusionSelect::make('status')->required(),
|
||||
Select::make('roles')->multiple()->relationship('roles', 'name')->preload(),
|
||||
UserFormFields::name(),
|
||||
UserFormFields::email(),
|
||||
UserFormFields::password(fn ($livewire) => $livewire instanceof Pages\CreateUser),
|
||||
UserFormFields::status(),
|
||||
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
|
||||
|
||||
namespace App\Models;
|
||||
namespace Modules\Conversation\App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Modules\User\App\Models\User;
|
||||
|
||||
class ConversationMessage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'conversation_id',
|
||||
'sender_id',
|
||||
'body',
|
||||
'read_at',
|
||||
];
|
||||
protected $fillable = ['conversation_id', 'sender_id', 'body', 'read_at'];
|
||||
|
||||
protected $casts = [
|
||||
'read_at' => 'datetime',
|
||||
];
|
||||
protected $casts = ['read_at' => 'datetime'];
|
||||
|
||||
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
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace Modules\Favorite\App\Http\Controllers;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\ConversationMessage;
|
||||
use App\Models\FavoriteSearch;
|
||||
use App\Models\User;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
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\User\App\Models\User;
|
||||
|
||||
class FavoriteController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$activeTab = (string) $request->string('tab', 'listings');
|
||||
|
||||
if (! in_array($activeTab, ['listings', 'searches', 'sellers'], true)) {
|
||||
$activeTab = 'listings';
|
||||
}
|
||||
|
||||
$statusFilter = (string) $request->string('status', 'all');
|
||||
|
||||
if (! in_array($statusFilter, ['all', 'active'], true)) {
|
||||
$statusFilter = 'all';
|
||||
}
|
||||
|
||||
$selectedCategoryId = $request->integer('category');
|
||||
|
||||
if ($selectedCategoryId <= 0) {
|
||||
$selectedCategoryId = null;
|
||||
}
|
||||
|
||||
$messageFilter = (string) $request->string('message_filter', 'all');
|
||||
if (! in_array($messageFilter, ['all', 'unread', 'important'], true)) {
|
||||
$messageFilter = 'all';
|
||||
}
|
||||
|
||||
$selectedCategoryId = $request->integer('category');
|
||||
if ($selectedCategoryId <= 0) {
|
||||
$selectedCategoryId = null;
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$categories = Category::query()
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
@ -49,12 +48,6 @@ class FavoriteController extends Controller
|
||||
$conversations = collect();
|
||||
$selectedConversation = null;
|
||||
$buyerConversationListingMap = [];
|
||||
$quickMessages = [
|
||||
'Merhaba',
|
||||
'İlan hâlâ satışta mı?',
|
||||
'Son fiyat nedir?',
|
||||
'Teşekkürler',
|
||||
];
|
||||
|
||||
if ($activeTab === 'listings') {
|
||||
$favoriteListings = $user->favoriteListings()
|
||||
@ -67,58 +60,18 @@ class FavoriteController extends Controller
|
||||
->withQueryString();
|
||||
|
||||
$userId = (int) $user->getKey();
|
||||
$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',
|
||||
'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();
|
||||
|
||||
$conversations = Conversation::inboxForUser($userId, $messageFilter);
|
||||
$buyerConversationListingMap = $conversations
|
||||
->where('buyer_id', $userId)
|
||||
->pluck('id', 'listing_id')
|
||||
->map(fn ($conversationId) => (int) $conversationId)
|
||||
->all();
|
||||
|
||||
$selectedConversationId = $request->integer('conversation');
|
||||
|
||||
if ($selectedConversationId <= 0 && $conversations->isNotEmpty()) {
|
||||
$selectedConversationId = (int) $conversations->first()->getKey();
|
||||
}
|
||||
|
||||
if ($selectedConversationId > 0) {
|
||||
$selectedConversation = $conversations->firstWhere('id', $selectedConversationId);
|
||||
$selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation'));
|
||||
|
||||
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(),
|
||||
]);
|
||||
$selectedConversation->loadThread();
|
||||
$selectedConversation->markAsReadFor($userId);
|
||||
|
||||
$conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation {
|
||||
if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) {
|
||||
@ -129,7 +82,6 @@ class FavoriteController extends Controller
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($activeTab === 'searches') {
|
||||
$favoriteSearches = $user->favoriteSearches()
|
||||
@ -149,7 +101,7 @@ class FavoriteController extends Controller
|
||||
->withQueryString();
|
||||
}
|
||||
|
||||
return view('favorites.index', [
|
||||
return view('favorite::index', [
|
||||
'activeTab' => $activeTab,
|
||||
'statusFilter' => $statusFilter,
|
||||
'selectedCategoryId' => $selectedCategoryId,
|
||||
@ -161,24 +113,15 @@ class FavoriteController extends Controller
|
||||
'conversations' => $conversations,
|
||||
'selectedConversation' => $selectedConversation,
|
||||
'buyerConversationListingMap' => $buyerConversationListingMap,
|
||||
'quickMessages' => $quickMessages,
|
||||
'quickMessages' => QuickMessageCatalog::all(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function toggleListing(Request $request, Listing $listing)
|
||||
{
|
||||
$user = $request->user();
|
||||
$isFavorite = $user->favoriteListings()->whereKey($listing->getKey())->exists();
|
||||
$isNowFavorite = $request->user()->toggleFavoriteListing($listing);
|
||||
|
||||
if ($isFavorite) {
|
||||
$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.');
|
||||
return back()->with('success', $isNowFavorite ? 'İlan favorilere eklendi.' : 'İlan favorilerden kaldırıldı.');
|
||||
}
|
||||
|
||||
public function toggleSeller(Request $request, User $seller)
|
||||
@ -189,17 +132,9 @@ class FavoriteController extends Controller
|
||||
return back()->with('error', 'Kendi hesabını favorilere ekleyemezsin.');
|
||||
}
|
||||
|
||||
$isFavorite = $user->favoriteSellers()->whereKey($seller->getKey())->exists();
|
||||
$isNowFavorite = $user->toggleFavoriteSeller($seller);
|
||||
|
||||
if ($isFavorite) {
|
||||
$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.');
|
||||
return back()->with('success', $isNowFavorite ? 'Satıcı favorilere eklendi.' : 'Satıcı favorilerden kaldırıldı.');
|
||||
}
|
||||
|
||||
public function storeSearch(Request $request)
|
||||
@ -225,15 +160,7 @@ class FavoriteController extends Controller
|
||||
$categoryName = Category::query()->whereKey($filters['category'])->value('name');
|
||||
}
|
||||
|
||||
$labelParts = [];
|
||||
if (! empty($filters['search'])) {
|
||||
$labelParts[] = '"'.$filters['search'].'"';
|
||||
}
|
||||
if ($categoryName) {
|
||||
$labelParts[] = $categoryName;
|
||||
}
|
||||
|
||||
$label = $labelParts !== [] ? implode(' · ', $labelParts) : 'Filtreli arama';
|
||||
$label = FavoriteSearch::labelFor($filters, is_string($categoryName) ? $categoryName : null);
|
||||
|
||||
$favoriteSearch = $request->user()->favoriteSearches()->firstOrCreate(
|
||||
['signature' => $signature],
|
||||
@ -1,23 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace Modules\Favorite\App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Modules\Category\Models\Category;
|
||||
use Modules\User\App\Models\User;
|
||||
|
||||
class FavoriteSearch extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'label',
|
||||
'search_term',
|
||||
'category_id',
|
||||
'filters',
|
||||
'signature',
|
||||
];
|
||||
protected $fillable = ['user_id', 'label', 'search_term', 'category_id', 'filters', 'signature'];
|
||||
|
||||
protected $casts = [
|
||||
'filters' => 'array',
|
||||
];
|
||||
protected $casts = ['filters' => 'array'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
@ -26,7 +19,7 @@ class FavoriteSearch extends Model
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(\Modules\Category\Models\Category::class);
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
public static function normalizeFilters(array $filters): array
|
||||
@ -45,4 +38,19 @@ class FavoriteSearch extends Model
|
||||
|
||||
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;
|
||||
|
||||
use App\Models\User;
|
||||
use Modules\User\App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
@ -11,9 +11,6 @@ use Modules\Listing\Models\Listing;
|
||||
|
||||
class ListingSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* @var array<int, array{title: string, description: string, price: int, city: string, country: string, image: string}>
|
||||
*/
|
||||
private const LISTINGS = [
|
||||
[
|
||||
'title' => 'iPhone 14 Pro 256 GB, temiz kullanılmış',
|
||||
@ -106,9 +103,6 @@ class ListingSeeder extends Seeder
|
||||
->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
|
||||
{
|
||||
$slug = Str::slug($data['title']) . '-' . ($index + 1);
|
||||
@ -167,7 +161,11 @@ class ListingSeeder extends Seeder
|
||||
|
||||
$media = $mediaItems->first();
|
||||
|
||||
if (! $media || (string) $media->file_name !== $targetFileName) {
|
||||
if (
|
||||
! $media
|
||||
|| (string) $media->file_name !== $targetFileName
|
||||
|| (string) $media->disk !== 'public'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
namespace Modules\Listing\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Conversation;
|
||||
use App\Models\FavoriteSearch;
|
||||
use Modules\Conversation\App\Models\Conversation;
|
||||
use Modules\Favorite\App\Models\FavoriteSearch;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
@ -55,18 +55,18 @@ class Listing extends Model implements HasMedia
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class);
|
||||
return $this->belongsTo(\Modules\User\App\Models\User::class);
|
||||
}
|
||||
|
||||
public function favoritedByUsers()
|
||||
{
|
||||
return $this->belongsToMany(\App\Models\User::class, 'favorite_listings')
|
||||
return $this->belongsToMany(\Modules\User\App\Models\User::class, 'favorite_listings')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -20,7 +20,7 @@ use Modules\Listing\Support\ListingPanelHelper;
|
||||
use Modules\Location\Models\City;
|
||||
use Modules\Location\Models\Country;
|
||||
use Modules\Partner\Filament\Resources\ListingResource;
|
||||
use Modules\Profile\Models\Profile;
|
||||
use Modules\User\App\Models\Profile;
|
||||
use Throwable;
|
||||
|
||||
class QuickCreateListing extends Page
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
namespace Modules\Partner\Providers;
|
||||
|
||||
use A909M\FilamentStateFusion\FilamentStateFusionPlugin;
|
||||
use App\Models\User;
|
||||
use Modules\User\App\Models\User;
|
||||
use DutchCodingCompany\FilamentDeveloperLogins\FilamentDeveloperLoginsPlugin;
|
||||
use DutchCodingCompany\FilamentSocialite\FilamentSocialitePlugin;
|
||||
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
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
namespace Modules\User\App\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function edit(Request $request): View
|
||||
{
|
||||
return view('user::profile.edit', ['user' => $request->user()]);
|
||||
}
|
||||
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validateWithBag('updateProfile', [
|
||||
'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);
|
||||
@ -28,12 +38,9 @@ class ProfileController extends Controller
|
||||
|
||||
$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
|
||||
{
|
||||
$request->validateWithBag('userDeletion', [
|
||||
@ -43,7 +50,6 @@ class ProfileController extends Controller
|
||||
$user = $request->user();
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
namespace Modules\Profile\Models;
|
||||
|
||||
namespace Modules\User\App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
@ -10,6 +11,7 @@ class Profile extends Model
|
||||
use LogsActivity;
|
||||
|
||||
protected $fillable = ['user_id', 'avatar', 'bio', 'phone', 'city', 'country', 'website', 'is_verified'];
|
||||
|
||||
protected $casts = ['is_verified' => 'boolean'];
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
@ -22,6 +24,6 @@ class Profile extends Model
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(\App\Models\User::class);
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use App\States\UserStatus;
|
||||
use Filament\Models\Contracts\HasAvatar;
|
||||
namespace Modules\User\App\Models;
|
||||
|
||||
use Filament\Models\Contracts\FilamentUser;
|
||||
use Filament\Models\Contracts\HasAvatar;
|
||||
use Filament\Models\Contracts\HasTenants;
|
||||
use Filament\Panel;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@ -14,6 +15,11 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Jeffgreco13\FilamentBreezy\Traits\TwoFactorAuthenticatable;
|
||||
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\Traits\LogsActivity;
|
||||
use Spatie\ModelStates\HasStates;
|
||||
@ -21,9 +27,16 @@ use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
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 $hidden = ['password', 'remember_token'];
|
||||
|
||||
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
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
@ -64,19 +82,22 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata
|
||||
|
||||
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()
|
||||
{
|
||||
return $this->belongsToMany(\Modules\Listing\Models\Listing::class, 'favorite_listings')
|
||||
->withTimestamps();
|
||||
return $this->belongsToMany(Listing::class, 'favorite_listings')->withTimestamps();
|
||||
}
|
||||
|
||||
public function favoriteSellers()
|
||||
{
|
||||
return $this->belongsToMany(self::class, 'favorite_sellers', 'user_id', 'seller_id')
|
||||
->withTimestamps();
|
||||
return $this->belongsToMany(self::class, 'favorite_sellers', 'user_id', 'seller_id')->withTimestamps();
|
||||
}
|
||||
|
||||
public function favoriteSearches()
|
||||
@ -111,8 +132,40 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata
|
||||
|
||||
public function getFilamentAvatarUrl(): ?string
|
||||
{
|
||||
return filled($this->avatar_url)
|
||||
? Storage::disk('public')->url($this->avatar_url)
|
||||
: null;
|
||||
return filled($this->avatar_url) ? 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
|
||||
|
||||
namespace App\States;
|
||||
namespace Modules\User\App\States;
|
||||
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasDescription;
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\States;
|
||||
namespace Modules\User\App\States;
|
||||
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasDescription;
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\States;
|
||||
namespace Modules\User\App\States;
|
||||
|
||||
use Filament\Support\Contracts\HasColor;
|
||||
use Filament\Support\Contracts\HasDescription;
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\States;
|
||||
namespace Modules\User\App\States;
|
||||
|
||||
use A909M\FilamentStateFusion\Concerns\StateFusionInfo;
|
||||
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="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-profile-information-form')
|
||||
@include('user::profile.partials.update-profile-information-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.update-password-form')
|
||||
@include('user::profile.partials.update-password-form')
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="max-w-xl">
|
||||
@include('profile.partials.delete-user-form')
|
||||
@include('user::profile.partials.delete-user-form')
|
||||
</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;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Modules\User\App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@ -15,19 +15,11 @@ use Illuminate\View\View;
|
||||
|
||||
class NewPasswordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the password reset view.
|
||||
*/
|
||||
public function create(Request $request): View
|
||||
{
|
||||
return view('auth.reset-password', ['request' => $request]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming new password request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
@ -36,9 +28,6 @@ class NewPasswordController extends Controller
|
||||
'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(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
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
|
||||
? redirect()->route('login')->with('status', __($status))
|
||||
: back()->withInput($request->only('email'))
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Modules\User\App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@ -14,19 +14,11 @@ use Illuminate\View\View;
|
||||
|
||||
class RegisteredUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the registration view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming registration request.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Modules\User\App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
@ -15,9 +15,6 @@ use Throwable;
|
||||
|
||||
class SocialAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $allowedProviders = ['google', 'facebook', 'apple'];
|
||||
|
||||
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 Modules\Listing\Models\Listing;
|
||||
use Modules\Category\Models\Category;
|
||||
use App\Models\User;
|
||||
use Modules\User\App\Models\User;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\ConversationMessage;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
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
|
||||
{
|
||||
$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\Location\Models\City;
|
||||
use Modules\Location\Models\Country;
|
||||
use Modules\Profile\Models\Profile;
|
||||
use Modules\User\App\Models\Profile;
|
||||
use Throwable;
|
||||
|
||||
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' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
'model' => env('AUTH_MODEL', Modules\User\App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
||||
@ -5,22 +5,14 @@ namespace Database\Factories;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\User\App\Models\User;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected $model = User::class;
|
||||
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
@ -32,9 +24,6 @@ class UserFactory extends Factory
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
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
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Modules\User\App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
@ -2,8 +2,10 @@
|
||||
"Category": true,
|
||||
"Listing": true,
|
||||
"Location": true,
|
||||
"Profile": true,
|
||||
"Admin": true,
|
||||
"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::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'])
|
||||
->name('logout');
|
||||
});
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\ConversationController;
|
||||
use App\Http\Controllers\FavoriteController;
|
||||
use App\Http\Controllers\HomeController;
|
||||
use App\Http\Controllers\LanguageController;
|
||||
use App\Http\Controllers\PanelController;
|
||||
@ -18,7 +16,6 @@ Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
|
||||
Route::get('/', [PanelController::class, 'index'])->name('index');
|
||||
Route::get('/ilanlarim', [PanelController::class, 'listings'])->name('listings.index');
|
||||
Route::get('/ilan-ver', [PanelController::class, 'create'])->name('listings.create');
|
||||
Route::get('/gelen-kutusu', [PanelController::class, 'inbox'])->name('inbox.index');
|
||||
Route::post('/ilanlarim/{listing}/kaldir', [PanelController::class, 'destroyListing'])->name('listings.destroy');
|
||||
Route::post('/ilanlarim/{listing}/satildi', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold');
|
||||
Route::post('/ilanlarim/{listing}/yeniden-yayinla', [PanelController::class, 'republishListing'])->name('listings.republish');
|
||||
@ -27,17 +24,4 @@ Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
|
||||
Route::get('/partner/{any?}', fn () => redirect()->route('panel.listings.index'))
|
||||
->where('any', '.*');
|
||||
|
||||
Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
|
||||
Route::get('/', [FavoriteController::class, 'index'])->name('index');
|
||||
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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user