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:
parent
a06fac4fef
commit
5cae04884a
43
app/Http/Controllers/FollowController.php
Normal file
43
app/Http/Controllers/FollowController.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,6 +27,7 @@ public function toArray(Request $request): array
|
|||||||
'totalRedirection' => $this->total_redirection,
|
'totalRedirection' => $this->total_redirection,
|
||||||
'isLiked' => $this->is_liked,
|
'isLiked' => $this->is_liked,
|
||||||
'isFavorite' => $this->is_favorite,
|
'isFavorite' => $this->is_favorite,
|
||||||
|
'isFollowed' => $this->is_followed
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,4 +44,9 @@ public function user(): MorphOne
|
|||||||
{
|
{
|
||||||
return $this->morphOne(User::class, 'role');
|
return $this->morphOne(User::class, 'role');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function followers(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Follow::class, 'broker_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,4 +35,9 @@ public function user(): MorphOne
|
|||||||
{
|
{
|
||||||
return $this->morphOne(User::class, 'role');
|
return $this->morphOne(User::class, 'role');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function followings(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Follow::class, 'customer_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,4 +167,30 @@ public function filterByCategory(Builder $query, string $category): Builder
|
|||||||
{
|
{
|
||||||
return $query->where('deal_category_id', $category);
|
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
24
app/Models/Follow.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,6 +29,7 @@ public function builder(): Builder
|
|||||||
// Check if the current user interacted with the deal
|
// Check if the current user interacted with the deal
|
||||||
->tap(fn ($q) => (new Deal)->withCurrentUserInteractions($q))
|
->tap(fn ($q) => (new Deal)->withCurrentUserInteractions($q))
|
||||||
->tap(fn ($q) => (new Deal)->withLikePerDeal($q))
|
->tap(fn ($q) => (new Deal)->withLikePerDeal($q))
|
||||||
|
->tap(fn ($q) => (new Deal)->withIsFollowedByCurrentUser($q))
|
||||||
->tap(fn ($q) => (new Deal)->withRedirectionPerDeal($q));
|
->tap(fn ($q) => (new Deal)->withRedirectionPerDeal($q));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -14,6 +14,7 @@ import {loadModalFromQuery} from "./explore-page.js";
|
|||||||
import {deleteRecentSearch} from "./deleteRecentSearch.js";
|
import {deleteRecentSearch} from "./deleteRecentSearch.js";
|
||||||
import {initNavMenu} from "./nav-menu.js";
|
import {initNavMenu} from "./nav-menu.js";
|
||||||
import {toggleShimmer} from "./shimmer.js";
|
import {toggleShimmer} from "./shimmer.js";
|
||||||
|
import {follow} from "./interaction.js";
|
||||||
|
|
||||||
document.deleteSearch = deleteRecentSearch;
|
document.deleteSearch = deleteRecentSearch;
|
||||||
document.like = like;
|
document.like = like;
|
||||||
@ -21,6 +22,7 @@ document.favorite = favorite;
|
|||||||
document.redirect = redirect;
|
document.redirect = redirect;
|
||||||
document.showReportModal = showReportModal;
|
document.showReportModal = showReportModal;
|
||||||
window.toggleShimmer = toggleShimmer;
|
window.toggleShimmer = toggleShimmer;
|
||||||
|
window.follow = follow;
|
||||||
|
|
||||||
window.addEventListener('load', async () => {
|
window.addEventListener('load', async () => {
|
||||||
const preloader = document.getElementById('preloader');
|
const preloader = document.getElementById('preloader');
|
||||||
|
|||||||
@ -88,6 +88,10 @@ function setDealDetails(dealDetails) {
|
|||||||
dealLink.classList.remove('flex');
|
dealLink.classList.remove('flex');
|
||||||
dealLink.classList.add('hidden');
|
dealLink.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set follow state
|
||||||
|
const followBtn = dealModal.querySelector('.followBtn');
|
||||||
|
followBtn.dataset.followed = deal.isFollowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setComments(dealId, dealModal) {
|
async function setComments(dealId, dealModal) {
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import {showToast} from "./toast.js";
|
import {showToast} from "./toast.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like a deal
|
||||||
|
* @param button
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
export async function like(button) {
|
export async function like(button) {
|
||||||
const activeClasses = ['fill-current', 'text-red-500']
|
const activeClasses = ['fill-current', 'text-red-500']
|
||||||
let isLiked = button.dataset.liked === 'true';
|
let isLiked = button.dataset.liked === 'true';
|
||||||
@ -18,7 +23,7 @@ export async function like(button) {
|
|||||||
showToast(response.data.message)
|
showToast(response.data.message)
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.response.status === 401){
|
if (e.response.status === 401) {
|
||||||
window.location.href = '/login/create';
|
window.location.href = '/login/create';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -29,6 +34,11 @@ export async function like(button) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark favorite a deal
|
||||||
|
* @param button
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
export async function favorite(button) {
|
export async function favorite(button) {
|
||||||
const activeClasses = ['fill-current', 'text-yellow-500']
|
const activeClasses = ['fill-current', 'text-yellow-500']
|
||||||
let isFavorite = button.dataset.favorite === 'true';
|
let isFavorite = button.dataset.favorite === 'true';
|
||||||
@ -42,7 +52,7 @@ export async function favorite(button) {
|
|||||||
showToast(response.data.message)
|
showToast(response.data.message)
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.response.status === 401){
|
if (e.response.status === 401) {
|
||||||
window.location.href = '/login/create';
|
window.location.href = '/login/create';
|
||||||
return;
|
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) {
|
export function redirect(url, id) {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
let redirectBadge = document.getElementById("redirectBadge".concat(id));
|
let redirectBadge = document.getElementById("redirectBadge".concat(id));
|
||||||
@ -96,3 +137,4 @@ function updateRedirectCount(badge, change) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 {{$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>
|
<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">
|
<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-name">{{$broker->name ?? ''}}</p>
|
||||||
<p data-is-loading="false" class="broker-email">{{$broker->email ?? ''}}</p>
|
<p data-is-loading="false" class="broker-email">{{$broker->email ?? ''}}</p>
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
</a>
|
</a>
|
||||||
@endguest
|
@endguest
|
||||||
@auth
|
@auth
|
||||||
<x-dashboard.user.broker-contact :broker="$broker"/>
|
<x-dashboard.user.broker-contact :broker="$broker" :is_followed="$deal->is_followed"/>
|
||||||
@endauth
|
@endauth
|
||||||
|
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\CommentController;
|
use App\Http\Controllers\CommentController;
|
||||||
|
use App\Http\Controllers\FollowController;
|
||||||
use App\Http\Controllers\Interaction\InteractionController;
|
use App\Http\Controllers\Interaction\InteractionController;
|
||||||
use App\Http\Controllers\Interaction\ReportController;
|
use App\Http\Controllers\Interaction\ReportController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
@ -15,3 +16,4 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
Route::apiResource('deals.comments', CommentController::class)->except(['update', 'edit', 'destroy']);
|
Route::apiResource('deals.comments', CommentController::class)->except(['update', 'edit', 'destroy']);
|
||||||
|
Route::post('/follow/{broker}', FollowController::class);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user