feat(user interaction): add favorite interaction

- refactor controller to handle favorite
- show like count and update when liked a deal
- maintain like and favorite state with server
This commit is contained in:
kusowl 2026-01-16 14:30:24 +05:30
parent 16c9ff3cee
commit f33f68cd3e
8 changed files with 179 additions and 46 deletions

View File

@ -18,18 +18,11 @@ public function __invoke()
protected function deals()
{
return Deal::query()
->where('active', true)
->select([
'id',
'title',
'description',
'image',
'active',
'slug',
'link',
'deal_category_id',
'user_id',
'id', 'title', 'description', 'image', 'active', 'slug', 'link',
'deal_category_id', 'user_id',
])
// Select additional details
->with([
'category:id,name',
'broker' => function ($query) {
@ -37,6 +30,11 @@ protected function deals()
->with('type');
},
])
// Select only admin-approved deals
->withActiveDeals()
// Check if the current user interacted with the deal
->withCurrentUserInteractions()
->withLikes()
->latest()
->paginate();
}

View File

@ -4,44 +4,63 @@
use App\Enums\InteractionType;
use App\Models\Deal;
use App\Models\Interaction;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
class InteractionController extends Controller
{
public function like(Deal $deal)
/**
* Interact to a deal by Like or Favorite state
* @param Deal $deal
* @param InteractionType $type [InteractionType::Like, InteractionType::Favorite]
* @return \Illuminate\Http\JsonResponse
*/
public function togglesState(Deal $deal, InteractionType $type)
{
if (!in_array($type, [InteractionType::Like, InteractionType::Favorite])) {
return response()->json(['error' => 'This interaction is not supported'], 400);
}
try {
// Check for existing like of the user with deal
$user = Auth::user();
$existingLike = $deal->interactions()
->where('user_id', $user->id)
->where('type', InteractionType::Like)
$existingInteraction = $deal->interactions()
->where('user_id', Auth::id())
->where('type', $type)
->first();
// Delete the existing like if exists
if ($existingLike) {
$existingLike->delete();
// Delete the existing like if exists, else add the like
$message = '';
if ($existingInteraction) {
$existingInteraction->delete();
$message = "{$type->value} removed from deal";
} else {
$data = [
'type' => $type,
'user_id' => Auth::id(),
];
return response()->json(['message' => 'Successfully unliked the post.']);
Interaction::unguard();
$deal->interactions()->create($data);
Interaction::reguard();
$message = "{$type->value} added to deal";
}
// Else, create a new like
$data = [
'type' => InteractionType::Like,
'user_id' => Auth::user()->id,
];
Deal::unguard();
$deal->interactions()->create($data);
Deal::reguard();
return response()->json(['message' => $message]);
} catch (\Throwable $e) {
Log::error('Error when liked a deal: id'.$deal->id.$e->getMessage(), $e->getTrace());
Log::error('Error when liked a deal',
[
'deal_id' => $deal->id,
'use_id' => Auth::id(),
'type' => $type,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]
);
return response()->json(['error' => 'Something went wrong.'], 500);
}
return response()->json(['message' => 'Successfully liked the post.']);
}
}

View File

@ -2,9 +2,12 @@
namespace App\Models;
use App\Enums\InteractionType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Auth;
class Deal extends Model
{
@ -22,4 +25,42 @@ public function interactions(): HasMany
{
return $this->hasMany(Interaction::class);
}
/**
* Get deals that are active
* @param Builder $query
* @return Builder
*/
public function scopeWithActiveDeals(Builder $query): Builder
{
return $query->where('active', true);
}
/**
* Get if the current user has liked or favorite the deal
* @param Builder $query
* @return Builder
*/
public function scopeWithCurrentUserInteractions(Builder $query): Builder
{
return $query->withExists([
'interactions as is_liked' => function ($query) {
$query->where('user_id', Auth::id())
->where('type', InteractionType::Like);
},
'interactions as is_favorite' => function ($query) {
$query->where('user_id', Auth::id())
->where('type', InteractionType::Favorite);
}
]);
}
public function scopeWithLikes(Builder $query): Builder
{
return $query->withCount([
'interactions as total_likes' => function ($query) {
$query->where('type', InteractionType::Like);
}
]);
}
}

View File

@ -1,14 +1,58 @@
async function like(e, id){
async function like(button, id) {
// Instant feedback for user
let likeBtns = button.querySelectorAll('.like');
let likeBadge = document.getElementById("likeBadge".concat(id));
toggleHidden(likeBtns);
// Check if user liked the deal
let isLiked = button.classList.contains('liked');
updateLikeCount(likeBadge, isLiked ? -1 : 1) ;
button.classList.toggle('liked')
try {
let response = await axios.post('/like/' + id);
if (response.status === 200) {
let likeBtns = e.querySelectorAll('.like');
likeBtns.forEach((e) => e.classList.toggle('hidden'))
if (response.status !== 200) {
// Revert the ui
toggleHidden(likeBtns);
}
} catch (e) {
toggleHidden(likeBtns);
console.error(e);
}
}
async function favorite(e, id) {
// Instant feedback for user
let favoriteBtns = e.querySelectorAll('.favorite');
toggleHidden(favoriteBtns);
try {
let response = await axios.post('/favorite/' + id);
if (response.status !== 200) {
// Revert the ui
toggleHidden(favoriteBtns);
}
} catch (e) {
toggleHidden(favoriteBtns);
console.error(e);
}
}
function toggleHidden(nodelist) {
nodelist.forEach((node) => node.classList.toggle('hidden'))
}
function updateLikeCount(badge, change){
try{
let likeCount = Math.max(parseInt( badge.dataset.count) + change, 0)
badge.querySelector('p').innerText = likeCount;
badge.dataset.count = likeCount.toString();
}
catch(e) {
console.error(e);
}
}
document.like = like;
document.favorite = favorite;

View File

@ -1,12 +1,35 @@
@props(['id'])
@props(['id', 'like' => false, 'favourite' => false])
<div class="">
<x-ui.button-sm class="text-accent-600" onclick="like(this, {{$id}})">
<x-heroicon-o-heart class="like w-4"/>
<x-heroicon-s-heart class="like w-4 hidden text-red-500"/>
<x-ui.button-sm @class(["text-accent-600", 'liked' => $like]) onclick="like(this, {{$id}})">
<x-heroicon-o-heart
@class([
"like w-4",
'hidden' => $like
])
/>
<x-heroicon-s-heart
@class([
"like w-4 text-red-500",
'hidden' => !$like
])
/>
</x-ui.button-sm>
<x-ui.button-sm class="text-accent-600">
<x-heroicon-o-star class="w-4"/>
<x-ui.button-sm class="text-accent-600" onclick="favorite(this, {{$id}})">
<x-heroicon-o-star
@class([
"favorite w-4",
'hidden' => $favourite
])
/>
<x-heroicon-s-star
@class([
"favorite w-4 text-yellow-500",
'hidden' => !$favourite
])
/>
</x-ui.button-sm>
<x-ui.button-sm class="text-accent-600">

View File

@ -5,7 +5,8 @@
<x-ui.button-sm variant="neutral">
{{$deal->category->name}}
</x-ui.button-sm>
<x-dashboard.user.action-toolbar :id="$deal->id" />
@ds($deal)
<x-dashboard.user.action-toolbar :id="$deal->id" :like="$deal->is_liked" :favourite="$deal->is_favorite" />
</div>
<p class="font-bold text-lg ">{{$deal->title}}</p>
@ -16,7 +17,7 @@
<div class="flex justify-between items-center">
<div class="flex space-x-3">
<x-dashboard.user.stat-badge :count="200">
<x-dashboard.user.stat-badge id="{{'likeBadge'.$deal->id}}" :count="$deal->total_likes">
<x-heroicon-o-heart class="w-4"/>
</x-dashboard.user.stat-badge>

View File

@ -1,5 +1,5 @@
@props(['count' => 0])
<div class="flex text-accent-600 space-x-1">
<div {{$attributes->merge(['class' =>"flex text-accent-600 space-x-1"])}} data-count="{{$count}}">
{{$slot}}
<p>{{$count}}</p>
</div>

View File

@ -1,5 +1,6 @@
<?php
use App\Enums\InteractionType;
use App\Enums\UserTypes;
use App\Http\Controllers\AuthenticatedUserController;
use App\Http\Controllers\Broker\BrokerDashboardController;
@ -41,7 +42,13 @@
Route::resource('profile', BrokerProfileController::class)->except('index', 'store', 'create');
});
Route::post('/like/{deal}', [InteractionController::class, 'like'])
Route::post('/like/{deal}', [InteractionController::class, 'togglesState'])
->defaults('type', InteractionType::Like)
->middleware('throttle:30,1')
->name('like');
Route::post('/favorite/{deal}', [InteractionController::class, 'togglesState'])
->defaults('type', InteractionType::Favorite)
->middleware('throttle:30,1')
->name('like');
});