From 5800f64f8884573e706039850c71b84f1eaa4585 Mon Sep 17 00:00:00 2001 From: AmirmahdiNourkazemi Date: Sun, 24 May 2026 23:21:00 +0330 Subject: [PATCH] feat: add like table --- app/Http/Controllers/LikeController.php | 234 ++++++++++++++++++ app/Models/Like.php | 56 +++++ app/Models/Media.php | 4 +- app/Models/Music.php | 7 +- app/Traits/HasLikes.php | 59 +++++ .../2026_05_24_192058_create_likes_table.php | 30 +++ routes/api.php | 12 + 7 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/LikeController.php create mode 100644 app/Models/Like.php create mode 100644 app/Traits/HasLikes.php create mode 100644 database/migrations/2026_05_24_192058_create_likes_table.php diff --git a/app/Http/Controllers/LikeController.php b/app/Http/Controllers/LikeController.php new file mode 100644 index 0000000..a600719 --- /dev/null +++ b/app/Http/Controllers/LikeController.php @@ -0,0 +1,234 @@ +validate([ + 'type' => 'required|string|in:music,media', + 'id' => 'required|integer', + ]); + + $model = $this->getModel($request->type, $request->id); + + if (!$model) { + return response()->json([ + 'message' => 'Item not found' + ], 404); + } + + $result = $model->addLike(); + + return response()->json([ + 'message' => 'Item liked successfully', + 'is_liked' => true, + 'likes_count' => $model->likes_count + ]); + } + + /** + * Unlike an item + */ + public function unlike(Request $request) + { + $request->validate([ + 'type' => 'required|string|in:music,media', + 'id' => 'required|integer', + ]); + + $model = $this->getModel($request->type, $request->id); + + if (!$model) { + return response()->json([ + 'message' => 'Item not found' + ], 404); + } + + $result = $model->removeLike(); + + return response()->json([ + 'message' => 'Item unliked successfully', + 'is_liked' => false, + 'likes_count' => $model->likes_count + ]); + } + + /** + * Toggle like status + */ + public function toggleLike(Request $request) + { + $request->validate([ + 'type' => 'required|string|in:music,media', + 'id' => 'required|integer', + ]); + + $model = $this->getModel($request->type, $request->id); + + if (!$model) { + return response()->json([ + 'message' => 'Item not found' + ], 404); + } + + $result = $model->toggleLike(); + + return response()->json([ + 'message' => $result ? 'Item liked successfully' : 'Item unliked successfully', + 'is_liked' => $model->is_liked, + 'likes_count' => $model->likes_count + ]); + } + + /** + * Get all liked items for the authenticated user + */ + public function myLikedItems(Request $request) + { + $type = $request->get('type'); // Optional filter by type (music or media) + + $query = Like::with('likeable') + ->where('user_id', auth()->id()); + + if ($type) { + $modelClass = $this->getModelClass($type); + $query->where('likeable_type', $modelClass); + } + + $likedItems = $query->latest()->paginate(20); + + return response()->json([ + 'data' => $likedItems, + 'total' => $likedItems->total(), + 'types' => [ + 'music' => 'App\\Models\\Music', + 'media' => 'App\\Models\\Media', + ] + ]); + // Transform the response + // $transformedItems = $likedItems->map(function ($likeItem) { + // $item = $likeItem->likeable; + + // if (!$item) return null; + + // $baseData = [ + // 'like_id' => $likeItem->id, + // 'liked_at' => $likeItem->created_at, + // 'type' => class_basename($likeItem->likeable_type), + // 'is_liked' => true, + // ]; + + // // Add type-specific data + // if ($item instanceof \App\Models\Music) { + // return array_merge($baseData, [ + // 'id' => $item->id, + // 'title' => $item->title, + // 'artist' => $item->artist, + // 'duration' => $item->duration_formatted ?? $item->duration, + // 'image_url' => $item->image_url, + // 'likes_count' => $item->likes_count, + // 'type_display' => 'music' + // ]); + // } elseif ($item instanceof \App\Models\Media) { + // return array_merge($baseData, [ + // 'id' => $item->id, + // 'title' => $item->title, + // 'caption' => $item->caption, + // 'media_type' => $item->type, + // 'duration' => $item->duration, + // 'image_url' => $item->image->url ?? null, + // 'likes_count' => $item->likes_count, + // 'type_display' => 'media' + // ]); + // } + + // return $baseData; + // })->filter(); + + // return response()->json([ + // 'data' => $transformedItems, + // 'total' => $likedItems->total(), + // 'current_page' => $likedItems->currentPage(), + // 'last_page' => $likedItems->lastPage(), + // 'per_page' => $likedItems->perPage(), + // ]); + } + + /** + * Check if specific item is liked by user + */ + public function checkLiked(Request $request) + { + $request->validate([ + 'type' => 'required|string|in:music,media', + 'id' => 'required|integer', + ]); + + $model = $this->getModel($request->type, $request->id); + + if (!$model) { + return response()->json([ + 'message' => 'Item not found' + ], 404); + } + + return response()->json([ + 'is_liked' => $model->is_liked, + 'likes_count' => $model->likes_count + ]); + } + + /** + * Get top liked items + */ + public function topLiked(Request $request) + { + $type = $request->get('type', 'music'); // Default to music + $limit = $request->get('limit', 10); + + $modelClass = $this->getModelClass($type); + + $items = $modelClass::with(['image']) + ->withCount('likes') + ->where(function($query) use ($modelClass) { + if (property_exists($modelClass, 'type')) { + $query->where('type', 'public'); + } + if (property_exists($modelClass, 'visibility')) { + $query->where('visibility', 'public'); + } + }) + ->orderBy('likes_count', 'desc') + ->limit($limit) + ->get(); + + return response()->json([ + 'data' => $items, + 'type' => $type, + 'total' => $items->count() + ]); + } + + private function getModel($type, $id) + { + $modelClass = $this->getModelClass($type); + return $modelClass::find($id); + } + + private function getModelClass($type) + { + $models = [ + 'music' => \App\Models\Music::class, + 'media' => \App\Models\Media::class, + ]; + + return $models[$type] ?? null; + } +} diff --git a/app/Models/Like.php b/app/Models/Like.php new file mode 100644 index 0000000..3449e09 --- /dev/null +++ b/app/Models/Like.php @@ -0,0 +1,56 @@ +belongsTo(User::class); + } + + public function likeable(): MorphTo + { + return $this->morphTo(); + } + + // Helper methods + public static function getLikedItemsForUser($userId, $type = null) + { + $query = self::with('likeable')->where('user_id', $userId); + + if ($type) { + $query->where('likeable_type', $type); + } + + return $query->latest()->get(); + } + + public static function isLikedByUser($userId, $likeableId, $likeableType) + { + return self::where([ + 'user_id' => $userId, + 'likeable_id' => $likeableId, + 'likeable_type' => $likeableType, + ])->exists(); + } + + public static function getLikeCount($likeableId, $likeableType) + { + return self::where([ + 'likeable_id' => $likeableId, + 'likeable_type' => $likeableType, + ])->count(); + } +} diff --git a/app/Models/Media.php b/app/Models/Media.php index 951ec5b..45e2e3c 100644 --- a/app/Models/Media.php +++ b/app/Models/Media.php @@ -33,7 +33,9 @@ class Media extends Model 'user_comment_id', 'has_user_rated', 'is_saved', - 'saved_count' + 'saved_count', + 'is_liked', + 'likes_count' ]; public function image() { diff --git a/app/Models/Music.php b/app/Models/Music.php index 0c7f384..fc9465d 100644 --- a/app/Models/Music.php +++ b/app/Models/Music.php @@ -11,10 +11,11 @@ use App\Traits\HasRatings; use App\Traits\HasComments; use App\Traits\HasSaves; +use App\Traits\HasLikes; class Music extends Model { - use HasFactory, HasRatings, HasComments , HasSaves; + use HasFactory, HasRatings, HasComments , HasSaves,HasLikes; protected $table = 'music'; @@ -54,7 +55,9 @@ public function tags(): BelongsToMany 'user_comment_id', 'has_user_rated' , 'is_saved', - 'saved_count' + 'saved_count', + 'is_liked', + 'likes_count' ]; public function getUrlAttribute() diff --git a/app/Traits/HasLikes.php b/app/Traits/HasLikes.php new file mode 100644 index 0000000..a97be1e --- /dev/null +++ b/app/Traits/HasLikes.php @@ -0,0 +1,59 @@ +morphMany(Like::class, 'likeable'); + } + + public function getIsLikedAttribute() + { + if (!auth()->check()) return false; + + return $this->likes() + ->where('user_id', auth()->id()) + ->exists(); + } + + public function getLikesCountAttribute() + { + return $this->likes()->count(); + } + + public function toggleLike() + { + if ($this->getIsLikedAttribute()) { + return $this->removeLike(); + } else { + return $this->addLike(); + } + } + + public function addLike() + { + if ($this->getIsLikedAttribute()) return false; + + return Like::create([ + 'user_id' => auth()->id(), + 'likeable_id' => $this->id, + 'likeable_type' => get_class($this), + ]); + } + + public function removeLike() + { + if (!$this->getIsLikedAttribute()) return false; + + return Like::where([ + 'user_id' => auth()->id(), + 'likeable_id' => $this->id, + 'likeable_type' => get_class($this), + ])->delete(); + } +} \ No newline at end of file diff --git a/database/migrations/2026_05_24_192058_create_likes_table.php b/database/migrations/2026_05_24_192058_create_likes_table.php new file mode 100644 index 0000000..ffca087 --- /dev/null +++ b/database/migrations/2026_05_24_192058_create_likes_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->morphs('likeable'); + $table->timestamps(); + $table->unique(['user_id', 'likeable_id', 'likeable_type']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('likes'); + } +}; diff --git a/routes/api.php b/routes/api.php index fae4480..c28a160 100644 --- a/routes/api.php +++ b/routes/api.php @@ -21,6 +21,7 @@ use App\Http\Controllers\CommentController; use App\Http\Controllers\MusicSubcategoryController; use App\Http\Controllers\SaveController; +use App\Http\Controllers\LikeController; Route::get('/test-hash', function() { $plain = 'amnk1380'; @@ -211,4 +212,15 @@ Route::get('/my-saved', [SaveController::class, 'mySavedItems']); Route::post('/check', [SaveController::class, 'checkSaved']); }); + + + // Like routes (only for music and media) + Route::prefix('likes')->group(function () { + Route::post('/like', [LikeController::class, 'like']); + Route::post('/unlike', [LikeController::class, 'unlike']); + Route::post('/toggle', [LikeController::class, 'toggleLike']); + Route::get('/my-liked', [LikeController::class, 'myLikedItems']); + Route::post('/check', [LikeController::class, 'checkLiked']); + Route::get('/top-liked', [LikeController::class, 'topLiked']); + }); }); \ No newline at end of file