feature(users can follow a broker)

- add schema and endpoints to make follows relationship with customer
and broker

- show and update states of follow button on ui
This commit is contained in:
kusowl 2026-02-05 18:12:36 +05:30
parent a06fac4fef
commit 5cae04884a
14 changed files with 193 additions and 5 deletions

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Models\Broker;
use App\Models\Follow;
class FollowController extends Controller
{
public function __invoke(Broker $broker)
{
$follow = $this->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();
}
}

View File

@ -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
];
}
}

View File

@ -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');
}
}

View File

@ -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');
}
}

View File

@ -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);
});
});
},
]);
}
}

24
app/Models/Follow.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Follow extends Model
{
protected $fillable = [
'customer_id',
'broker_id',
];
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function broker(): BelongsTo
{
return $this->belongsTo(Broker::class);
}
}

View File

@ -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));
}
}

View File

@ -0,0 +1,25 @@
<?php
use App\Models\Broker;
use App\Models\Customer;
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('follows', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Customer::class);
$table->foreignIdFor(Broker::class);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('follows');
}
};

View File

@ -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');

View File

@ -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) {

View File

@ -1,5 +1,10 @@
import {showToast} from "./toast.js";
/**
* Like a deal
* @param button
* @returns {Promise<void>}
*/
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<void>}
*/
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<void>}
*/
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);
}
}

View File

@ -1,6 +1,12 @@
@props(['broker' => ''])
@props(['broker' => '', 'is_followed' => false])
<div {{$attributes->merge(['class' => "p-4 text-sm bg-gray-100 border-gray-200 border rounded-xl"])}}>
<div class="flex space-x-4 items-baseline">
<p class="font-bold mb-2">Broker Contact</p>
<x-ui.button-sm data-is-loading="false" data-followed="{{$is_followed ? 'true' : 'false'}}" onclick="follow(this, {{$broker->role_id ?? ''}})" class="followBtn group p-0! mt-0.5">
<span class="group-data-[followed=true]:hidden text-blue-600">Follow</span>
<span class="group-data-[followed=false]:hidden text-accent-600">Unfollow</span>
</x-ui.button-sm>
</div>
<div class="text-accent-600 space-y-1">
<p data-is-loading="false" class="broker-name">{{$broker->name ?? ''}}</p>
<p data-is-loading="false" class="broker-email">{{$broker->email ?? ''}}</p>

View File

@ -24,7 +24,7 @@
</a>
@endguest
@auth
<x-dashboard.user.broker-contact :broker="$broker"/>
<x-dashboard.user.broker-contact :broker="$broker" :is_followed="$deal->is_followed"/>
@endauth
<div class="flex justify-between items-center">

View File

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\CommentController;
use App\Http\Controllers\FollowController;
use App\Http\Controllers\Interaction\InteractionController;
use App\Http\Controllers\Interaction\ReportController;
use Illuminate\Support\Facades\Route;
@ -15,3 +16,4 @@
});
Route::apiResource('deals.comments', CommentController::class)->except(['update', 'edit', 'destroy']);
Route::post('/follow/{broker}', FollowController::class);