diff --git a/app/Http/Controllers/FollowController.php b/app/Http/Controllers/FollowController.php new file mode 100644 index 0000000..da1a95b --- /dev/null +++ b/app/Http/Controllers/FollowController.php @@ -0,0 +1,43 @@ +checkFollow($broker); + if ($follow === null) { + return $this->store($broker); + } else { + return $this->destroy($follow); + } + } + + public function store(Broker $broker) + { + Follow::create([ + 'broker_id' => $broker->id, + 'customer_id' => auth()->id(), + ]); + + return response()->json(['message' => 'Followed successfully.']); + } + + public function destroy(Follow $follow) + { + $follow->delete(); + + return response()->json(['message' => 'Unfollowed successfully.']); + } + + protected function checkFollow(Broker $broker) + { + return Follow::where('broker_id', $broker->id) + ->where('customer_id', auth()->id()) + ->first(); + } +} diff --git a/app/Http/Resources/DealResource.php b/app/Http/Resources/DealResource.php index 9fec5f6..c7f63a3 100644 --- a/app/Http/Resources/DealResource.php +++ b/app/Http/Resources/DealResource.php @@ -27,6 +27,7 @@ public function toArray(Request $request): array 'totalRedirection' => $this->total_redirection, 'isLiked' => $this->is_liked, 'isFavorite' => $this->is_favorite, + 'isFollowed' => $this->is_followed ]; } } diff --git a/app/Models/Broker.php b/app/Models/Broker.php index 402e2dc..a002928 100644 --- a/app/Models/Broker.php +++ b/app/Models/Broker.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphOne; /** @@ -43,4 +44,9 @@ public function user(): MorphOne { return $this->morphOne(User::class, 'role'); } + + public function followers(): HasMany + { + return $this->hasMany(Follow::class, 'broker_id'); + } } diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 08834d0..66b82c8 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphOne; /** @@ -34,4 +35,9 @@ public function user(): MorphOne { return $this->morphOne(User::class, 'role'); } + + public function followings(): HasMany + { + return $this->hasMany(Follow::class, 'customer_id'); + } } diff --git a/app/Models/Deal.php b/app/Models/Deal.php index 411adc7..e636782 100644 --- a/app/Models/Deal.php +++ b/app/Models/Deal.php @@ -167,4 +167,30 @@ public function filterByCategory(Builder $query, string $category): Builder { return $query->where('deal_category_id', $category); } + + // Add this to App\Models\Deal.php + + /** + * Scope a query to check if the current user follows the deal's broker + */ + #[Scope] + public function withIsFollowedByCurrentUser(Builder $query): Builder + { + $user = Auth::user(); + + if (! $user || $user->role_type !== \App\Models\Customer::class) { + return $query->withExists(['broker as is_followed' => fn ($q) => $q->whereRaw('1 = 0')]); + } + + return $query->withExists([ + 'broker as is_followed' => function ($query) use ($user) { + $query->where('role_type', \App\Models\Broker::class) + ->whereHasMorph('type', [\App\Models\Broker::class], function ($query) use ($user) { + $query->whereHas('followers', function ($query) use ($user) { + $query->where('customer_id', $user->id); + }); + }); + }, + ]); + } } diff --git a/app/Models/Follow.php b/app/Models/Follow.php new file mode 100644 index 0000000..f3e64c8 --- /dev/null +++ b/app/Models/Follow.php @@ -0,0 +1,24 @@ +belongsTo(Customer::class); + } + + public function broker(): BelongsTo + { + return $this->belongsTo(Broker::class); + } +} diff --git a/app/Queries/ExplorePageDealsQuery.php b/app/Queries/ExplorePageDealsQuery.php index 8c2728a..0ceb1f7 100644 --- a/app/Queries/ExplorePageDealsQuery.php +++ b/app/Queries/ExplorePageDealsQuery.php @@ -29,6 +29,7 @@ public function builder(): Builder // Check if the current user interacted with the deal ->tap(fn ($q) => (new Deal)->withCurrentUserInteractions($q)) ->tap(fn ($q) => (new Deal)->withLikePerDeal($q)) + ->tap(fn ($q) => (new Deal)->withIsFollowedByCurrentUser($q)) ->tap(fn ($q) => (new Deal)->withRedirectionPerDeal($q)); } } diff --git a/database/migrations/2026_02_05_085807_create_follows_table.php b/database/migrations/2026_02_05_085807_create_follows_table.php new file mode 100644 index 0000000..b278cfe --- /dev/null +++ b/database/migrations/2026_02_05_085807_create_follows_table.php @@ -0,0 +1,25 @@ +id(); + $table->foreignIdFor(Customer::class); + $table->foreignIdFor(Broker::class); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('follows'); + } +}; diff --git a/resources/js/app.js b/resources/js/app.js index 700492f..fa0e154 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -14,6 +14,7 @@ import {loadModalFromQuery} from "./explore-page.js"; import {deleteRecentSearch} from "./deleteRecentSearch.js"; import {initNavMenu} from "./nav-menu.js"; import {toggleShimmer} from "./shimmer.js"; +import {follow} from "./interaction.js"; document.deleteSearch = deleteRecentSearch; document.like = like; @@ -21,6 +22,7 @@ document.favorite = favorite; document.redirect = redirect; document.showReportModal = showReportModal; window.toggleShimmer = toggleShimmer; +window.follow = follow; window.addEventListener('load', async () => { const preloader = document.getElementById('preloader'); diff --git a/resources/js/deal-view-modal.js b/resources/js/deal-view-modal.js index 61ec118..e759c30 100644 --- a/resources/js/deal-view-modal.js +++ b/resources/js/deal-view-modal.js @@ -88,6 +88,10 @@ function setDealDetails(dealDetails) { dealLink.classList.remove('flex'); dealLink.classList.add('hidden'); } + + // set follow state + const followBtn = dealModal.querySelector('.followBtn'); + followBtn.dataset.followed = deal.isFollowed; } async function setComments(dealId, dealModal) { diff --git a/resources/js/interaction.js b/resources/js/interaction.js index 228cbbb..322d050 100644 --- a/resources/js/interaction.js +++ b/resources/js/interaction.js @@ -1,5 +1,10 @@ import {showToast} from "./toast.js"; +/** + * Like a deal + * @param button + * @returns {Promise} + */ export async function like(button) { const activeClasses = ['fill-current', 'text-red-500'] let isLiked = button.dataset.liked === 'true'; @@ -18,7 +23,7 @@ export async function like(button) { showToast(response.data.message) } catch (e) { - if (e.response.status === 401){ + if (e.response.status === 401) { window.location.href = '/login/create'; return; } @@ -29,6 +34,11 @@ export async function like(button) { } } +/** + * Mark favorite a deal + * @param button + * @returns {Promise} + */ export async function favorite(button) { const activeClasses = ['fill-current', 'text-yellow-500'] let isFavorite = button.dataset.favorite === 'true'; @@ -42,7 +52,7 @@ export async function favorite(button) { showToast(response.data.message) } catch (e) { - if (e.response.status === 401){ + if (e.response.status === 401) { window.location.href = '/login/create'; return; } @@ -52,6 +62,37 @@ export async function favorite(button) { } } +/** + * Follow a broker + * @param button + * @param brokerId + * @returns {Promise} + */ +export async function follow(button, brokerId) { + let isFollowed = button.dataset.followed === 'true'; + try { + button.dataset.followed = isFollowed ? 'false' : 'true'; + // Update other buttons + const selector = `button[onclick="follow(this, ${brokerId})"]`; + + document.querySelectorAll(selector).forEach(btn => { + btn.dataset.followed = isFollowed ? 'false' : 'true'; + }); + + let response = await axios.post(`/api/follow/${brokerId}`); + showToast(response.data.message); + } catch (e) { + button.dataset.followed = isFollowed ? 'true' : 'false'; + showToast('Something went wrong!') + console.error(e) + } +} + +/** + * Increment visit count of a deal's external link + * @param url + * @param id + */ export function redirect(url, id) { window.open(url, '_blank'); let redirectBadge = document.getElementById("redirectBadge".concat(id)); @@ -96,3 +137,4 @@ function updateRedirectCount(badge, change) { 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 33c8fd9..e241bbd 100644 --- a/resources/views/components/dashboard/user/broker-contact.blade.php +++ b/resources/views/components/dashboard/user/broker-contact.blade.php @@ -1,6 +1,12 @@ -@props(['broker' => '']) +@props(['broker' => '', 'is_followed' => false])
merge(['class' => "p-4 text-sm bg-gray-100 border-gray-200 border rounded-xl"])}}> -

Broker Contact

+
+

Broker Contact

+ +

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

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

diff --git a/resources/views/components/dashboard/user/listing-card.blade.php b/resources/views/components/dashboard/user/listing-card.blade.php index 69ed5ef..72f1991 100644 --- a/resources/views/components/dashboard/user/listing-card.blade.php +++ b/resources/views/components/dashboard/user/listing-card.blade.php @@ -24,7 +24,7 @@ @endguest @auth - + @endauth
diff --git a/routes/api/interactions.php b/routes/api/interactions.php index 54bc6c7..43288f1 100644 --- a/routes/api/interactions.php +++ b/routes/api/interactions.php @@ -1,6 +1,7 @@ except(['update', 'edit', 'destroy']); +Route::post('/follow/{broker}', FollowController::class);