From 94ef8f360d99ef71d52d5d55f48778e1fddba4f9 Mon Sep 17 00:00:00 2001 From: kusowl Date: Fri, 23 Jan 2026 16:14:04 +0530 Subject: [PATCH] feature (favorite and reported deals): - add favorites and reported tabs in user profile pages - add remove favorites - customers can view a deal directly from profiles section and deal modal is shown in explore page - fix formatting by pint --- .env.example | 2 +- app/Actions/GetUserFavoritesAction.php | 20 ++++ app/Actions/GetUserReportedDealsAction.php | 17 ++++ .../Auth/RegisteredUserController.php | 8 +- .../Broker/BrokerProfileController.php | 1 - .../Controllers/ExplorePageController.php | 4 +- .../Interaction/InteractionController.php | 16 +++- .../Interaction/ReportController.php | 17 +++- .../User/UserProfileController.php | 17 ++-- app/Models/Customer.php | 1 + app/Models/Deal.php | 1 - app/Models/User.php | 10 ++ app/Services/ProfileInitialsService.php | 3 +- ...26_01_22_125826_create_customers_table.php | 3 +- resources/js/app.js | 8 +- resources/js/bootstrap.js | 3 + resources/js/deal-view-modal.js | 2 +- resources/js/explore-page.js | 11 +++ resources/js/tab.js | 32 +++++++ .../dashboard/user/broker-contact.blade.php | 6 +- .../dashboard/user/listing.blade.php | 2 +- .../views/components/ui/button-sm.blade.php | 1 + .../views/components/ui/tab/button.blade.php | 4 + .../views/components/ui/tab/content.blade.php | 3 + .../views/components/ui/tab/index.blade.php | 8 ++ .../views/components/ui/table/head.blade.php | 3 + .../views/components/ui/table/index.blade.php | 6 ++ .../views/components/ui/table/row.blade.php | 8 ++ .../components/ui/toggle-button.blade.php | 3 +- .../dashboards/user/profile/show.blade.php | 95 ++++++++++++++++++- routes/api/deals.php | 1 - routes/web/interaction.php | 4 +- 32 files changed, 284 insertions(+), 36 deletions(-) create mode 100644 app/Actions/GetUserFavoritesAction.php create mode 100644 app/Actions/GetUserReportedDealsAction.php create mode 100644 resources/js/explore-page.js create mode 100644 resources/js/tab.js create mode 100644 resources/views/components/ui/tab/button.blade.php create mode 100644 resources/views/components/ui/tab/content.blade.php create mode 100644 resources/views/components/ui/tab/index.blade.php create mode 100644 resources/views/components/ui/table/head.blade.php create mode 100644 resources/views/components/ui/table/index.blade.php create mode 100644 resources/views/components/ui/table/row.blade.php diff --git a/.env.example b/.env.example index c0660ea..38241be 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -APP_NAME=Laravel +APP_NAME=DealHub APP_ENV=local APP_KEY= APP_DEBUG=true diff --git a/app/Actions/GetUserFavoritesAction.php b/app/Actions/GetUserFavoritesAction.php new file mode 100644 index 0000000..7de6075 --- /dev/null +++ b/app/Actions/GetUserFavoritesAction.php @@ -0,0 +1,20 @@ +interactedDeals() + ->where('interactions.type', InteractionType::Favorite) + ->tap(fn ($q) => (new Deal)->withActiveDeals($q)) + ->select('deals.id', 'deals.title') + ->get(); + } +} diff --git a/app/Actions/GetUserReportedDealsAction.php b/app/Actions/GetUserReportedDealsAction.php new file mode 100644 index 0000000..5f3d78f --- /dev/null +++ b/app/Actions/GetUserReportedDealsAction.php @@ -0,0 +1,17 @@ +reports() + ->select('reports.id') + ->with('deals:id,title') + ->get(); + } +} diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 7374d52..20d9fda 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -25,22 +25,22 @@ public function store(StoreRegisterdUser $request) DB::transaction(function () use ($data) { switch ($data['role']) { case UserTypes::Broker->value: - { + $data['status'] = UserStatus::Pending->value; // Create Broker first, then link the user $broker = Broker::create(); $broker->user()->create($data); break; - } + case UserTypes::User->value: - { + $data['status'] = UserStatus::Active->value; $customer = Customer::create(); $customer->user()->create($data); break; - } + } }); diff --git a/app/Http/Controllers/Broker/BrokerProfileController.php b/app/Http/Controllers/Broker/BrokerProfileController.php index 18a4901..f2f6216 100644 --- a/app/Http/Controllers/Broker/BrokerProfileController.php +++ b/app/Http/Controllers/Broker/BrokerProfileController.php @@ -9,7 +9,6 @@ use App\Services\ProfileInitialsService; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Str; class BrokerProfileController extends Controller { diff --git a/app/Http/Controllers/ExplorePageController.php b/app/Http/Controllers/ExplorePageController.php index ec1a1fb..3c3f0b9 100644 --- a/app/Http/Controllers/ExplorePageController.php +++ b/app/Http/Controllers/ExplorePageController.php @@ -33,7 +33,7 @@ protected function deals(FormRequest $request, Builder $query, AddRecentSearchAc { // Add a search query if ($request->has('search') && $request->get('search') !== null) { - $query->tap(fn($q) => (new Deal)->search($q, $request->search)); + $query->tap(fn ($q) => (new Deal)->search($q, $request->search)); \Illuminate\Support\defer(function () use ($action, $request) { $action->execute($request->user(), ['query' => $request->search]); @@ -42,7 +42,7 @@ protected function deals(FormRequest $request, Builder $query, AddRecentSearchAc // Add category sorting filter if ($request->has('category') && $request->get('category') !== null) { - $query->tap(fn($q) => (new Deal)->filterByCategory($q, $request->category)); + $query->tap(fn ($q) => (new Deal)->filterByCategory($q, $request->category)); } // Add sorting filters diff --git a/app/Http/Controllers/Interaction/InteractionController.php b/app/Http/Controllers/Interaction/InteractionController.php index 824a9ab..31fd599 100644 --- a/app/Http/Controllers/Interaction/InteractionController.php +++ b/app/Http/Controllers/Interaction/InteractionController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Models\Deal; use App\Models\Interaction; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; @@ -15,9 +16,8 @@ class InteractionController extends Controller * Interact to a deal by Like or Favorite state * * @param InteractionType $type [InteractionType::Like, InteractionType::Favorite] - * @return \Illuminate\Http\JsonResponse */ - public function togglesState(Deal $deal, InteractionType $type) + public function togglesState(Deal $deal, InteractionType $type, Request $request) { if (! in_array($type, [InteractionType::Like, InteractionType::Favorite])) { return response()->json(['error' => 'This interaction is not supported'], 400); @@ -47,8 +47,11 @@ public function togglesState(Deal $deal, InteractionType $type) $message = ucfirst($type->value).' added to deal'; } - - return response()->json(['message' => $message]); + if ($request->expectsJson()) { + return response()->json(['message' => $message]); + } else { + return back()->with('success', $message); + } } catch (\Throwable $e) { Log::error('Error when liked a deal', @@ -60,8 +63,11 @@ public function togglesState(Deal $deal, InteractionType $type) 'trace' => $e->getTraceAsString(), ] ); + if ($request->expectsJson()) { + return response()->json(['error' => 'Something went wrong.'], 500); + } - return response()->json(['error' => 'Something went wrong.'], 500); + return back()->with('error', 'Something went wrong.'); } } diff --git a/app/Http/Controllers/Interaction/ReportController.php b/app/Http/Controllers/Interaction/ReportController.php index 7e5dc51..8834bec 100644 --- a/app/Http/Controllers/Interaction/ReportController.php +++ b/app/Http/Controllers/Interaction/ReportController.php @@ -78,6 +78,21 @@ public function update(Request $request, Report $report) */ public function destroy(Report $report) { - // + try { + DB::transaction(function () use ($report) { + $report->deals()->detach(); + $report->delete(); + }); + + return back()->with('success', 'Report deleted successfully.'); + } catch (\Throwable $e) { + Log::error('Error deleting report', [ + 'user_id' => Auth::id(), + 'report_id' => $report->id, + 'error' => $e->getMessage(), + ]); + } + + return back()->with('error', 'Something went wrong.'); } } diff --git a/app/Http/Controllers/User/UserProfileController.php b/app/Http/Controllers/User/UserProfileController.php index 94e02e7..d2ca10c 100644 --- a/app/Http/Controllers/User/UserProfileController.php +++ b/app/Http/Controllers/User/UserProfileController.php @@ -2,13 +2,12 @@ namespace App\Http\Controllers\User; +use App\Actions\GetUserFavoritesAction; +use App\Actions\GetUserReportedDealsAction; use App\Http\Controllers\Controller; -use App\Http\Requests\StoreBrokerProfileRequest; use App\Http\Requests\StoreCustomerProfileRequest; -use App\Models\Broker; use App\Models\User; use App\Services\ProfileInitialsService; -use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -17,8 +16,12 @@ class UserProfileController extends Controller /** * Display the specified resource. */ - public function show(User $profile, ProfileInitialsService $service) - { + public function show( + User $profile, + ProfileInitialsService $service, + GetUserFavoritesAction $favoritesAction, + GetUserReportedDealsAction $reportedDealsAction + ) { // Get the user profile $user = $profile->type; @@ -32,7 +35,9 @@ public function show(User $profile, ProfileInitialsService $service) ->with('initials', $initials) ->with('location', $user->location) ->with('bio', $user->bio) - ->with('phone', $user->phone); + ->with('phone', $user->phone) + ->with('favorites', $favoritesAction->execute($profile)) + ->with('reported', $reportedDealsAction->execute($profile)); } /** diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 39390da..f36054a 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -8,6 +8,7 @@ class Customer extends Model { protected $fillable = ['bio', 'location', 'phone']; + public function user(): MorphOne { return $this->morphOne(User::class, 'role'); diff --git a/app/Models/Deal.php b/app/Models/Deal.php index 5c347f0..cec6c4b 100644 --- a/app/Models/Deal.php +++ b/app/Models/Deal.php @@ -118,5 +118,4 @@ public function filterByCategory(Builder $query, string $category): Builder { return $query->where('deal_category_id', $category); } - } diff --git a/app/Models/User.php b/app/Models/User.php index eebf54c..cffb2cd 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -89,4 +89,14 @@ public function dealsInteractions(): HasManyThrough { return $this->hasManyThrough(Interaction::class, Deal::class); } + + public function interactedDeals(): HasManyThrough + { + return $this->hasManyThrough(Deal::class, Interaction::class, 'user_id', 'id', 'id', 'deal_id'); + } + + public function reports(): HasMany + { + return $this->hasMany(Report::class); + } } diff --git a/app/Services/ProfileInitialsService.php b/app/Services/ProfileInitialsService.php index 7032af0..fffce13 100644 --- a/app/Services/ProfileInitialsService.php +++ b/app/Services/ProfileInitialsService.php @@ -6,7 +6,6 @@ class ProfileInitialsService { - /** * Create the initials from a full name (e.g. John Doe, Alex Mark, jane clerk) * to display on the profile page (e.g. JD, AM, JC). @@ -15,7 +14,7 @@ public function create(string $fullname) { return Str::of($fullname) ->explode(' ') - ->map(fn($word) => Str::substr(ucfirst($word), 0, 1)) + ->map(fn ($word) => Str::substr(ucfirst($word), 0, 1)) ->join(''); } } diff --git a/database/migrations/2026_01_22_125826_create_customers_table.php b/database/migrations/2026_01_22_125826_create_customers_table.php index 6c9d77b..ceab23b 100644 --- a/database/migrations/2026_01_22_125826_create_customers_table.php +++ b/database/migrations/2026_01_22_125826_create_customers_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ /** * Run the migrations. */ diff --git a/resources/js/app.js b/resources/js/app.js index a062cce..91a3aae 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -9,13 +9,15 @@ import "./toast.js" import "./deal-view-modal.js" import {favorite, like, redirect} from "./interaction.js"; import {showReportModal} from "./report-deal.js"; +import {initTabs} from "./tab.js"; +import {loadModalFromQuery} from "./explore-page.js"; document.like = like; document.favorite = favorite; document.redirect = redirect; document.showReportModal = showReportModal; -window.addEventListener('load', () => { +window.addEventListener('load', async () => { const preloader = document.getElementById('preloader'); const content = document.getElementById('content'); @@ -32,4 +34,8 @@ window.addEventListener('load', () => { if (savedState) { setSidebarState(savedState); } + + initTabs(); + + await loadModalFromQuery(); }); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 5f1390b..c3b7e3e 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -1,4 +1,7 @@ import axios from 'axios'; + window.axios = axios; window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; +window.axios.defaults.headers.common['Accept'] = 'application/json'; +window.axios.defaults.headers.common['Content-Type'] = 'application/json'; diff --git a/resources/js/deal-view-modal.js b/resources/js/deal-view-modal.js index 28592df..05868ea 100644 --- a/resources/js/deal-view-modal.js +++ b/resources/js/deal-view-modal.js @@ -3,7 +3,7 @@ import {closeModal, showModal} from "@/modal.js"; import {redirect} from "./interaction.js"; import {toggleShimmer} from "./shimmer.js"; -async function showDealModal(dealId) { +export async function showDealModal(dealId) { if (!dealId) { showToast('Something went wrong!'); return; diff --git a/resources/js/explore-page.js b/resources/js/explore-page.js new file mode 100644 index 0000000..92af6d9 --- /dev/null +++ b/resources/js/explore-page.js @@ -0,0 +1,11 @@ +import {showDealModal} from "./deal-view-modal.js"; + +export async function loadModalFromQuery(){ + const query = new URLSearchParams(window.location.search); + if (query.has('show')) { + const dealId = query.get('show'); + if(dealId){ + await showDealModal(dealId); + } + } +} diff --git a/resources/js/tab.js b/resources/js/tab.js new file mode 100644 index 0000000..d9348ec --- /dev/null +++ b/resources/js/tab.js @@ -0,0 +1,32 @@ +export function initTabs() { + try { + const tabs = document.querySelectorAll('.tabs'); + tabs.forEach(tab => { + const tabBtns = tab.querySelectorAll('.tab-btn'); + tabBtns.forEach(tabBtn => + tabBtn.addEventListener('click', (e) => { + // Make the current button active + tabBtns.forEach(btn => btn.classList.remove('bg-white')); + tabBtn.classList.add('bg-white'); + + // Hide all other tabs + const tabContents = tab.querySelectorAll('.tab-content'); + tabContents.forEach(content => content.removeAttribute('data-show')); + + // Show the target tab + const targetContent = tabBtn.dataset.target; + if (targetContent) { + const contentTabData = `[data-tab="${targetContent}"]`; + const tabContent = tab.querySelector(contentTabData); + if (tabContent) { + tabContent.setAttribute('data-show', '') + } + } + } + ) + ); + }) + } catch (e) { + console.error(e) + } +} diff --git a/resources/views/components/dashboard/user/broker-contact.blade.php b/resources/views/components/dashboard/user/broker-contact.blade.php index 854a2f4..9bda292 100644 --- a/resources/views/components/dashboard/user/broker-contact.blade.php +++ b/resources/views/components/dashboard/user/broker-contact.blade.php @@ -2,9 +2,9 @@

Broker Contact

-

{{$broker->name ?? ''}}

-

{{$broker->email ?? ''}}

-

{{$broker->role->phone ?? ''}}

+

{{$broker->name ?? ''}}

+

{{$broker->email ?? ''}}

+

{{$broker->role->phone ?? ''}}

diff --git a/resources/views/components/dashboard/user/listing.blade.php b/resources/views/components/dashboard/user/listing.blade.php index ee2ce87..96ca84f 100644 --- a/resources/views/components/dashboard/user/listing.blade.php +++ b/resources/views/components/dashboard/user/listing.blade.php @@ -1,4 +1,4 @@ -@props(['deals' => []]) +@props(['deals' => [], 'isInteractive'])
@forelse($deals as $deal) diff --git a/resources/views/components/ui/button-sm.blade.php b/resources/views/components/ui/button-sm.blade.php index 2b935ad..fd885d0 100644 --- a/resources/views/components/ui/button-sm.blade.php +++ b/resources/views/components/ui/button-sm.blade.php @@ -3,6 +3,7 @@ $variants = [ 'neutral' => 'bg-primary-600 text-white', 'ghost' => 'bg-gray-100 text-black text-sm', + 'red' => 'bg-red-500 text-red-100 text-sm' ]; $variantClass = $variants[$variant] ?? ''; diff --git a/resources/views/components/ui/tab/button.blade.php b/resources/views/components/ui/tab/button.blade.php new file mode 100644 index 0000000..5ead179 --- /dev/null +++ b/resources/views/components/ui/tab/button.blade.php @@ -0,0 +1,4 @@ +@props(['active' => false]) +merge(['class' => 'tab-btn'])}}> + {{$slot}} + diff --git a/resources/views/components/ui/tab/content.blade.php b/resources/views/components/ui/tab/content.blade.php new file mode 100644 index 0000000..069d168 --- /dev/null +++ b/resources/views/components/ui/tab/content.blade.php @@ -0,0 +1,3 @@ +
merge(['class' => 'tab-content [&:not([data-show])]:hidden'])}}> + {{$slot}} +
diff --git a/resources/views/components/ui/tab/index.blade.php b/resources/views/components/ui/tab/index.blade.php new file mode 100644 index 0000000..dfd79e3 --- /dev/null +++ b/resources/views/components/ui/tab/index.blade.php @@ -0,0 +1,8 @@ +
merge(['class' => 'tabs'])}} > + + {{$buttons ?? ''}} + +
+ {{$slot}} +
+
diff --git a/resources/views/components/ui/table/head.blade.php b/resources/views/components/ui/table/head.blade.php new file mode 100644 index 0000000..69b4646 --- /dev/null +++ b/resources/views/components/ui/table/head.blade.php @@ -0,0 +1,3 @@ + +{{$slot}} + diff --git a/resources/views/components/ui/table/index.blade.php b/resources/views/components/ui/table/index.blade.php new file mode 100644 index 0000000..5c4a82c --- /dev/null +++ b/resources/views/components/ui/table/index.blade.php @@ -0,0 +1,6 @@ +
+ + {{$slot}} +
+
+ diff --git a/resources/views/components/ui/table/row.blade.php b/resources/views/components/ui/table/row.blade.php new file mode 100644 index 0000000..50e0949 --- /dev/null +++ b/resources/views/components/ui/table/row.blade.php @@ -0,0 +1,8 @@ + + {{$slot}} + @if(($actions ?? '') !== '') + + {{$actions ?? ''}} + + @endif + diff --git a/resources/views/components/ui/toggle-button.blade.php b/resources/views/components/ui/toggle-button.blade.php index dc377c8..cda97c5 100644 --- a/resources/views/components/ui/toggle-button.blade.php +++ b/resources/views/components/ui/toggle-button.blade.php @@ -1,8 +1,7 @@ @props(['active' => false]) @aware(['activeColor' => 'bg-white'])
class(["rounded-full hover:bg-gray-100 hover:border-gray-300 border border-transparent transition-colors duration-300 ease-in-out", $activeColor => $active])}} + {{$attributes->class(["rounded-full hover:border-gray-300 border border-transparent transition-colors duration-300 ease-in-out", $activeColor => $active])}} > {{ $slot }}
diff --git a/resources/views/dashboards/user/profile/show.blade.php b/resources/views/dashboards/user/profile/show.blade.php index 2b68a8c..4d78605 100644 --- a/resources/views/dashboards/user/profile/show.blade.php +++ b/resources/views/dashboards/user/profile/show.blade.php @@ -15,7 +15,23 @@ class="border border-accent-600/40"> -
+ @session('error') +
+ + {{$value}} + +
+ @endsession + + @session('success') +
+ + {{$value}} + +
+ @endsession + +
@@ -60,5 +76,82 @@ class="w-25 h-25 rounded-xl bg-linear-150 from-[#305afc] to-[#941dfb] text-5xl t
+ + + +
+ +

Favorites

+
+
+ +
+ +

Reported Deals

+
+
+
+ + + +

Your favorite deals

+ + + Deals + + @forelse($favorites as $deal) + + {{$deal->title}} + +
+ + + +
+ @csrf + + + +
+
+
+
+ @empty + + No Deals found + + @endforelse +
+
+
+ + +

Your reported deals

+ + + Deals + + @forelse($reported as $report) + + {{$report->deals->first()->title}} + +
+ + + +
+
+
+ @empty + + No Deals found + + @endforelse +
+
+
+
diff --git a/routes/api/deals.php b/routes/api/deals.php index e497ddd..ea3872a 100644 --- a/routes/api/deals.php +++ b/routes/api/deals.php @@ -5,7 +5,6 @@ use App\Queries\ExplorePageDealsQuery; Route::get('/deals/{deal}', function (Deal $deal) { - sleep(2); return new DealResource( (new ExplorePageDealsQuery) ->builder() diff --git a/routes/web/interaction.php b/routes/web/interaction.php index 98da730..79582aa 100644 --- a/routes/web/interaction.php +++ b/routes/web/interaction.php @@ -17,9 +17,9 @@ ->middleware('throttle:30,1') ->name('favorite'); - Route::post('/report/{deal}', [ReportController::class, 'store']) + Route::resource('report', ReportController::class) ->middleware('throttle:5,1') - ->name('report.store'); + ->only(['store', 'destroy']); Route::get('/redirect/{deal}', [InteractionController::class, 'redirect']) ->middleware(['throttle:10,1', 'signed'])