refactor: consolidate alert components and improve functionality

- replaced `alert-error` and `alert-success` components with a single reusable `alert` component
- added JS functionality for dismissible alerts
- updated related views to use the new `alert` component
- adjusted broker profile logic to display initials and verification status dynamically
- refactored morph relations from `type` to `role`
- enhanced image preview behavior for file inputs
- made broker migration fields nullable and added safeguards against registration errors
- Added confirmation when a user wants delete deal
- Add dynamic initials for user profile picture
- make image file name non-overidding with timestamp
This commit is contained in:
kusowl 2026-01-14 12:19:20 +05:30
parent 1275a74506
commit ccb5a2ed5e
18 changed files with 102 additions and 40 deletions

View File

@ -3,8 +3,10 @@
namespace App\Http\Controllers\Broker; namespace App\Http\Controllers\Broker;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Broker;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str;
class BrokerProfileController extends Controller class BrokerProfileController extends Controller
{ {
@ -13,10 +15,31 @@ class BrokerProfileController extends Controller
*/ */
public function show(User $profile) public function show(User $profile)
{ {
// Get the broker profile
$broker = $profile->type;
// TODO: move this to middleware
if (! $broker instanceof Broker) {
abort(403, 'This user is not a broker.');
}
/**
* Create the initials from a full name (e.g. John Doe, Alex Mark, jane clerk)
* to display on profile page (e.g. JD, AM, JC).
*/
$initials = Str::of($profile->name)
->explode(' ')
->map(fn ($word) => Str::substr(ucfirst($word), 0, 1))
->join('');
return view('dashboards.broker.profile.show') return view('dashboards.broker.profile.show')
->with('name', $profile->name) ->with('name', $profile->name)
->with('joinDate', $profile->created_at->format('F Y')) ->with('joinDate', $profile->created_at->format('F Y'))
->with('email', $profile->email); ->with('email', $profile->email)
->with('initials', $initials)
->with('verified', $broker->verified)
->with('location', $broker->location)
->with('phone', $broker->phone);
} }
/** /**

View File

@ -5,7 +5,10 @@
use App\Enums\UserStatus; use App\Enums\UserStatus;
use App\Enums\UserTypes; use App\Enums\UserTypes;
use App\Http\Requests\StoreRegisterdUser; use App\Http\Requests\StoreRegisterdUser;
use App\Models\Broker;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class RegisteredUserController extends Controller class RegisteredUserController extends Controller
{ {
@ -18,17 +21,30 @@ public function store(StoreRegisterdUser $request)
{ {
$data = $request->validated(); $data = $request->validated();
$status = UserStatus::Active->value; try {
DB::transaction(function () use ($data) {
if ($data['role'] === UserTypes::Broker->value) { if ($data['role'] === UserTypes::Broker->value) {
$status = UserStatus::Pending->value; $data['status'] = UserStatus::Pending->value;
}
$data['status'] = $status; // Create Broker first, then the User linked to it
$broker = Broker::create();
$broker->user()->create($data);
} else {
$data['status'] = UserStatus::Active->value;
User::create($data); User::create($data);
}
});
return to_route('login.create') return to_route('login.create')
->with('userRegistered', 'User registered successfully.'); ->with('userRegistered', 'User registered successfully.');
} catch (\Throwable $e) {
Log::error('Registration Failed: '.$e->getMessage());
return back()
->withInput()
->with('error', 'Something went wrong during registration.');
}
} }
} }

View File

@ -11,12 +11,11 @@ protected function casts(): array
{ {
return [ return [
'verified' => 'boolean', 'verified' => 'boolean',
'active' => 'boolean',
]; ];
} }
public function user(): MorphOne public function user(): MorphOne
{ {
return $this->morphOne(User::class, 'type'); return $this->morphOne(User::class, 'role');
} }
} }

View File

@ -63,10 +63,10 @@ public function deals(): HasMany
} }
/** /**
* Returns model of User's type * Returns model of User's role type
*/ */
public function type(): MorphTo public function type(): MorphTo
{ {
return $this->morphTo(); return $this->morphTo('role');
} }
} }

View File

@ -9,7 +9,10 @@ class FileService
{ {
public function upload(UploadedFile $file, string $folder, string $filename): string public function upload(UploadedFile $file, string $folder, string $filename): string
{ {
return $file->storeAs($folder, $filename.'.'.$file->extension(), 'public'); // This prevents overriding of same image name
$filename = time().$filename.'.'.$file->extension();
return $file->storeAs($folder, $filename, 'public');
} }
public function delete(?string $path): void public function delete(?string $path): void

View File

@ -11,7 +11,7 @@
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
// $middleware->redirectGuestsTo(fn () => route('login.create'));
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //

View File

@ -13,11 +13,10 @@ public function up(): void
{ {
Schema::create('brokers', function (Blueprint $table) { Schema::create('brokers', function (Blueprint $table) {
$table->id(); $table->id();
$table->text('bio'); $table->text('bio')->nullable();
$table->string('location'); $table->string('location')->nullable();
$table->string('phone')->nullable(); $table->string('phone')->nullable();
$table->boolean('verified')->default(false); $table->boolean('verified')->default(false);
$table->boolean('active')->default(true);
$table->timestamps(); $table->timestamps();
}); });
} }

View File

@ -12,7 +12,7 @@
public function up(): void public function up(): void
{ {
Schema::table('users', function (Blueprint $table) { Schema::table('users', function (Blueprint $table) {
$table->nullableMorphs('type'); $table->nullableMorphs('role');
}); });
} }
@ -22,7 +22,7 @@ public function up(): void
public function down(): void public function down(): void
{ {
Schema::table('users', function (Blueprint $table) { Schema::table('users', function (Blueprint $table) {
$table->dropMorphs('type'); $table->dropMorphs('role');
}); });
} }
}; };

5
resources/js/alert.js Normal file
View File

@ -0,0 +1,5 @@
function removeAlert(child){
const parentAlert = child.closest('.alert');
parentAlert.style.display = "none";
}
document.removeAlert = removeAlert;

View File

@ -31,6 +31,7 @@ function upload(size) {
closeModalBtn.addEventListener('click', () => { closeModalBtn.addEventListener('click', () => {
// this closes then modal and sets the preview image // this closes then modal and sets the preview image
previewImage.src = imageUrl; previewImage.src = imageUrl;
previewImage.style.display = "block";
modal.close(); modal.close();
}) })

View File

@ -15,15 +15,15 @@ class="bg-linear-135 h-screen from-[#EFF6FF] to-[#FCF3F8] flex flex-col justify-
</div> </div>
@session('error') @session('error')
<x-ui.alert-error> <x-ui.alert variant="error">
{{$value}} {{$value}}
</x-ui.alert-error> </x-ui.alert>
@endsession @endsession
@session('userRegistered') @session('userRegistered')
<x-ui.alert-success> <x-ui.alert variant="success">
{{$value}} {{$value}}
</x-ui.alert-success> </x-ui.alert>
@endsession @endsession
<form action="{{route('login.store')}}" method="post" class="flex flex-col space-y-5"> <form action="{{route('login.store')}}" method="post" class="flex flex-col space-y-5">

View File

@ -33,10 +33,10 @@ class="text-xs underline text-accent-601 mt-1">
<x-ui.button :link="route('broker.deals.edit', $id)" class="w-full border border-accent-600/30" <x-ui.button :link="route('broker.deals.edit', $id)" class="w-full border border-accent-600/30"
icon="pencil-square">Edit icon="pencil-square">Edit
</x-ui.button> </x-ui.button>
<form class="w-full" method="post" action="{{route('broker.deals.destroy', $id)}}"> <form class="w-full" onsubmit="return confirm('Are you sure to delete this ?')" method="post" action="{{route('broker.deals.destroy', $id)}}">
@csrf @csrf
@method("DELETE") @method("DELETE")
<x-ui.button variant="red" class="w-full" icon="trash">Delete</x-ui.button> <x-ui.button variant="red" class="w-full" icon="trash" >Delete</x-ui.button>
</form> </form>
</div> </div>
</div> </div>

View File

@ -1,3 +0,0 @@
<div class="py-2 px-4 bg-red-100 rounded-lg text-xs font-bold text-red-950">
{{$slot}}
</div>

View File

@ -1,3 +0,0 @@
<div class="py-2 px-4 bg-green-100 rounded-lg text-xs font-bold text-green-950">
{{$slot}}
</div>

View File

@ -0,0 +1,21 @@
@props(['variant' => 'neutral'])
@php
$variants = [
'neutral' => 'bg-black text-white',
'success' => 'bg-green-100 text-green-950',
'error' => 'bg-red-100 text-red-950'
];
$variantClasses = $variants[$variant];
@endphp
<div class="alert flex items-center space-x-4 py-2 px-4 rounded-lg text-xs font-bold {{$variantClasses}} ">
<div class="flex-1">
{{$slot}}
</div>
<div>
<x-ui.button-sm onclick="removeAlert(this)">
<x-heroicon-o-x-mark class="w-4"/>
</x-ui.button-sm>
</div>
</div>
@vite('resources/js/alert.js')

View File

@ -13,7 +13,7 @@
<div <div
class="p-8 border-2 border-dashed border-accent-600/70 rounded-lg flex flex-col space-y-2 justify-center items-center overflow-hidden min-h-40 bg-gray-50 relative"> class="p-8 border-2 border-dashed border-accent-600/70 rounded-lg flex flex-col space-y-2 justify-center items-center overflow-hidden min-h-40 bg-gray-50 relative">
<img src="{{ $value }}" id="preview-image" <img src="{{ $value }}" id="preview-image" alt="" onerror="this.style.display='none'"
class="absolute inset-0 w-full h-full object-cover opacity-60 group-hover:opacity-10 transition-opacity"> class="absolute inset-0 w-full h-full object-cover opacity-60 group-hover:opacity-10 transition-opacity">
<x-heroicon-o-arrow-up-tray class="w-8 text-accent-600/70 z-10"/> <x-heroicon-o-arrow-up-tray class="w-8 text-accent-600/70 z-10"/>
@ -26,7 +26,7 @@ class="absolute inset-0 w-full h-full object-cover opacity-60 group-hover:opacit
<input <input
name="{{$name}}" name="{{$name}}"
id="image-input" id="image-input"
class="opacity-0 absolute w-full h-full top-0 left-0 cursor-pointer" class="opacity-0 absolute w-full h-full top-0 left-0 cursor-pointer z-20"
type="file" type="file"
accept="{{ $allowed ? '.' . str_replace(',', ',.', $allowed) : 'image/*' }}" accept="{{ $allowed ? '.' . str_replace(',', ',.', $allowed) : 'image/*' }}"
onchange="upload(10)" onchange="upload(10)"

View File

@ -4,11 +4,11 @@
<div class="wrapper"> <div class="wrapper">
@session('success') @session('success')
<x-ui.alert-success>{{$value}}</x-ui.alert-success> <x-ui.alert variant="success">{{$value}}</x-ui.alert>
@endsession @endsession
@session('error') @session('error')
<x-ui.alert-error>{{$value}}</x-ui.alert-error> <x-ui.alert variant="error">{{$value}}</x-ui.alert>
@endsession @endsession
</div> </div>

View File

@ -16,7 +16,8 @@
<div class="grid grid-cols-8 gap-6"> <div class="grid grid-cols-8 gap-6">
<div class="col-span-8 place-self-start md:col-span-1 flex items-center justify-center w-full"> <div class="col-span-8 place-self-start md:col-span-1 flex items-center justify-center w-full">
<div class="w-25 h-25 rounded-xl bg-linear-150 from-[#305afc] to-[#941dfb] text-5xl text-white flex justify-center items-center">UR <div class="w-25 h-25 rounded-xl bg-linear-150 from-[#305afc] to-[#941dfb] text-5xl text-white flex justify-center items-center">
{{$initials}}
</div> </div>
</div> </div>
@ -25,7 +26,7 @@
<div class=""> <div class="">
<p class="text-3xl font-bold">{{$name ?? 'Name'}}</p> <p class="text-3xl font-bold">{{$name ?? 'Name'}}</p>
<div class="flex space-x-1 mt-2"> <div class="flex space-x-1 mt-2">
<x-ui.button-sm variant="neutral">Verified Broker</x-ui.button-sm> <x-ui.button-sm variant="neutral">{{$verified ? 'Verified Broker' : 'Not Verified'}}</x-ui.button-sm>
<x-heroicon-s-star class="ml-2 w-4 fill-amber-300"/> <x-heroicon-s-star class="ml-2 w-4 fill-amber-300"/>
<p>4.8</p> <p>4.8</p>
</div> </div>