12 Commits
37 changed files with 1746 additions and 172 deletions
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use Illuminate\Http\Request;
class CategoryController extends Controller
{
public function index(Request $request)
{
$query = Category::query()->withCount('subcategories');
if ($request->boolean('with_subcategories')) {
$query->with('subcategories');
}
return response()->json(
$query->orderBy('name')->get()
);
}
public function store(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:255|unique:categories,name',
]);
$category = Category::create($data);
return response()->json([
'message' => 'Category created successfully',
'category' => $category,
], 201);
}
public function show($id)
{
$category = Category::with('subcategories')->withCount('subcategories')->findOrFail($id);
return response()->json($category);
}
public function update(Request $request, $id)
{
$category = Category::findOrFail($id);
$data = $request->validate([
'name' => 'required|string|max:255|unique:categories,name,' . $category->id,
]);
$category->update($data);
return response()->json([
'message' => 'Category updated successfully',
'category' => $category,
]);
}
public function destroy($id)
{
$category = Category::findOrFail($id);
$category->delete();
return response()->json(['message' => 'Category deleted successfully']);
}
}
+2 -1
View File
@@ -127,10 +127,11 @@ private function getModelClass($type)
$models = [ $models = [
'music' => \App\Models\Music::class, 'music' => \App\Models\Music::class,
'media' => \App\Models\Media::class, 'media' => \App\Models\Media::class,
'playlist' => \App\Models\MusicPlaylist::class,
]; ];
if (!isset($models[$type])) { if (!isset($models[$type])) {
abort(404, 'Invalid model type'); abort(404, 'Invalid model type. Supported types: music, media, playlist');
} }
return $models[$type]; return $models[$type];
+236
View File
@@ -0,0 +1,236 @@
<?php
namespace App\Http\Controllers;
use App\Models\Like;
use Illuminate\Http\Request;
class LikeController extends Controller
{
/**
* Like an item (music or media)
*/
public function like(Request $request)
{
$request->validate([
'type' => 'required|string|in:music,media,playlist',
'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,playlist',
'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,playlist',
'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',
'playlist' => 'App\\Models\\MusicPlaylist',
]
]);
// 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,playlist',
'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,
'playlist' => \App\Models\MusicPlaylist::class,
];
return $models[$type] ?? null;
}
}
+69 -51
View File
@@ -4,6 +4,7 @@
use App\Models\Media; use App\Models\Media;
use App\Models\Category; use App\Models\Category;
use App\Models\SubCategory;
use App\Models\Tag; use App\Models\Tag;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -18,8 +19,10 @@ public function store(Request $request)
'title' => 'required|string|max:255', 'title' => 'required|string|max:255',
'caption' => 'nullable|string', 'caption' => 'nullable|string',
'type' => 'required|in:audio,video', 'type' => 'required|in:audio,video',
'category_id' => 'nullable|exists:categories,id', 'category_ids' => 'nullable|array',
'category_name' => 'nullable|string|max:255', 'category_ids.*' => 'integer|exists:categories,id',
'subcategory_ids' => 'nullable|array',
'subcategory_ids.*' => 'integer|exists:sub_categories,id',
'image_id' => 'nullable|exists:images,id', 'image_id' => 'nullable|exists:images,id',
'duration' => 'nullable|integer', 'duration' => 'nullable|integer',
'is_premium' => 'nullable|boolean', 'is_premium' => 'nullable|boolean',
@@ -31,14 +34,6 @@ public function store(Request $request)
'tags.*' => 'string', 'tags.*' => 'string',
]); ]);
$categoryId = $data['category_id'] ?? null;
if (!$categoryId && isset($data['category_name'])) {
$category = Category::firstOrCreate([
'name' => $data['category_name']
]);
$categoryId = $category->id;
}
$path = null; $path = null;
if ($request->hasFile('file')) { if ($request->hasFile('file')) {
$path = $request->file('file')->store('media', 'public'); $path = $request->file('file')->store('media', 'public');
@@ -52,11 +47,14 @@ public function store(Request $request)
'file_path' => $path, 'file_path' => $path,
'external_url' => $data['external_url'] ?? null, 'external_url' => $data['external_url'] ?? null,
'image_id' => $data['image_id'] ?? null, 'image_id' => $data['image_id'] ?? null,
'category_id' => $categoryId,
'duration' => $data['duration'] ?? null, 'duration' => $data['duration'] ?? null,
'visibility' => $data['visibility'] ?? 'public', 'visibility' => $data['visibility'] ?? 'public',
'is_premium'=> $data['is_premium'] ?? false 'is_premium'=> $data['is_premium'] ?? false
]); ]);
$media->categories()->sync($data['category_ids'] ?? []);
$media->subCategories()->sync($data['subcategory_ids'] ?? []);
if (!empty($data['tags'])) { if (!empty($data['tags'])) {
$tagIds = []; $tagIds = [];
@@ -69,12 +67,12 @@ public function store(Request $request)
} }
return response()->json([ return response()->json([
'message' => 'Media created successfully', 'message' => 'Media created successfully',
'media' => $media->load(['image', 'category' , 'tags']), 'media' => $media->load(['image', 'categories', 'subCategories', 'tags']),
]); ]);
} }
public function index(Request $request) public function index(Request $request)
{ {
$query = Media::with(['image', 'category', 'myNote', 'tags','comments']) $query = Media::with(['image', 'categories', 'subCategories', 'myNote', 'tags','comments'])
->where(function ($q) { ->where(function ($q) {
$q->where('visibility', 'public') $q->where('visibility', 'public')
->orWhere('user_id', auth()->id()); ->orWhere('user_id', auth()->id());
@@ -88,7 +86,16 @@ public function index(Request $request)
if ($request->filled('categories')) { if ($request->filled('categories')) {
$categories = explode(',', $request->categories); $categories = explode(',', $request->categories);
$query->whereIn('category_id', $categories); $query->whereHas('categories', function ($q) use ($categories) {
$q->whereIn('categories.id', $categories);
});
}
if ($request->filled('subcategories')) {
$subcategories = explode(',', $request->subcategories);
$query->whereHas('subCategories', function ($q) use ($subcategories) {
$q->whereIn('sub_categories.id', $subcategories);
});
} }
/* /*
@@ -164,9 +171,12 @@ public function index(Request $request)
$q->where('title', 'LIKE', "%$search%") $q->where('title', 'LIKE', "%$search%")
->orWhere('caption', 'LIKE', "%$search%") ->orWhere('caption', 'LIKE', "%$search%")
->orWhereHas('category', function ($c) use ($search) { ->orWhereHas('categories', function ($c) use ($search) {
$c->where('name', 'LIKE', "%$search%"); $c->where('name', 'LIKE', "%$search%");
}) })
->orWhereHas('subCategories', function ($s) use ($search) {
$s->where('name', 'LIKE', "%$search%");
})
->orWhereHas('tags', function ($t) use ($search) { ->orWhereHas('tags', function ($t) use ($search) {
$t->where('name', 'LIKE', "%$search%"); $t->where('name', 'LIKE', "%$search%");
}); });
@@ -266,21 +276,28 @@ public function filters(Request $request)
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
$categories = Category::select( $visibleMedia = function ($q) {
'categories.id', $q->where(function ($inner) {
'categories.name', $inner->where('media.visibility', 'public')
DB::raw('COUNT(media.id) as media_count') ->orWhere('media.user_id', auth()->id());
) });
->leftJoin('media', function ($join) { };
$join->on('categories.id', '=', 'media.category_id')
->where(function ($q) { $categories = Category::query()
$q->where('media.visibility', 'public') ->withCount(['media as media_count' => $visibleMedia])
->orWhere('media.user_id', auth()->id());
});
})
->groupBy('categories.id', 'categories.name')
->orderByDesc('media_count') ->orderByDesc('media_count')
->get(); ->get(['id', 'name']);
/*
|--------------------------------------------------------------------------
| 1️⃣.5 Subcategories with media count
|--------------------------------------------------------------------------
*/
$subcategories = SubCategory::query()
->withCount(['media as media_count' => $visibleMedia])
->orderByDesc('media_count')
->get(['id', 'category_id', 'name']);
/* /*
@@ -314,17 +331,19 @@ public function filters(Request $request)
->get(); ->get();
return response()->json([ return response()->json([
'categories' => $categories, 'categories' => $categories,
'durations' => $durations, 'subcategories' => $subcategories,
'durations' => $durations,
]); ]);
} }
public function show($id) public function show($id)
{ {
$media = Media::with([ $media = Media::with([
'image', 'image',
'category', 'categories',
'myNote', 'subCategories',
'myNote',
'tags', 'tags',
'comments' => function($query) { 'comments' => function($query) {
$query->with('user')->latest()->limit(10); $query->with('user')->latest()->limit(10);
@@ -360,7 +379,8 @@ public function show($id)
'updated_at' => $media->updated_at, 'updated_at' => $media->updated_at,
'is_premium' => $media->is_premium, 'is_premium' => $media->is_premium,
'image' => $media->image, 'image' => $media->image,
'category' => $media->category, 'categories' => $media->categories,
'sub_categories' => $media->subCategories,
'tags' => $media->tags, 'tags' => $media->tags,
'myNote' => $media->myNote, 'myNote' => $media->myNote,
'is_saved' => $media->is_saved, 'is_saved' => $media->is_saved,
@@ -442,9 +462,10 @@ public function update(Request $request, $id)
'caption' => 'nullable|string', 'caption' => 'nullable|string',
'type' => 'nullable|in:audio,video', 'type' => 'nullable|in:audio,video',
// support both: category_id or category_name 'category_ids' => 'nullable|array',
'category_id' => 'nullable|exists:categories,id', 'category_ids.*' => 'integer|exists:categories,id',
'category_name' => 'nullable|string|max:255', 'subcategory_ids' => 'nullable|array',
'subcategory_ids.*' => 'integer|exists:sub_categories,id',
'is_premium' => 'nullable|boolean', 'is_premium' => 'nullable|boolean',
'image_id' => 'nullable|exists:images,id', 'image_id' => 'nullable|exists:images,id',
'duration' => 'nullable|integer', 'duration' => 'nullable|integer',
@@ -455,16 +476,6 @@ public function update(Request $request, $id)
'tags.*' => 'string', 'tags.*' => 'string',
]); ]);
// --- handle auto-create category ---
$categoryId = $data['category_id'] ?? $media->category_id;
if (isset($data['category_name'])) {
$category = Category::firstOrCreate([
'name' => $data['category_name']
]);
$categoryId = $category->id;
}
// --- handle file replace --- // --- handle file replace ---
if ($request->hasFile('file')) { if ($request->hasFile('file')) {
Storage::disk('public')->delete($media->file_path); Storage::disk('public')->delete($media->file_path);
@@ -476,7 +487,6 @@ public function update(Request $request, $id)
'title' => $data['title'] ?? $media->title, 'title' => $data['title'] ?? $media->title,
'caption' => $data['caption'] ?? $media->caption, 'caption' => $data['caption'] ?? $media->caption,
'type' => $data['type'] ?? $media->type, 'type' => $data['type'] ?? $media->type,
'category_id' => $categoryId,
'is_premium' => $data['is_premium'] ?? $media->is_premium, 'is_premium' => $data['is_premium'] ?? $media->is_premium,
'duration' => $data['duration'] ?? $media->duration, 'duration' => $data['duration'] ?? $media->duration,
'external_url' => $data['external_url'] ?? $media->external_url, 'external_url' => $data['external_url'] ?? $media->external_url,
@@ -496,6 +506,14 @@ public function update(Request $request, $id)
// --- update media --- // --- update media ---
$media->update($updateData); $media->update($updateData);
if (array_key_exists('category_ids', $data)) {
$media->categories()->sync($data['category_ids'] ?? []);
}
if (array_key_exists('subcategory_ids', $data)) {
$media->subCategories()->sync($data['subcategory_ids'] ?? []);
}
if (isset($data['tags'])) { if (isset($data['tags'])) {
$tagIds = []; $tagIds = [];
@@ -509,7 +527,7 @@ public function update(Request $request, $id)
return response()->json([ return response()->json([
'message' => 'Media updated successfully', 'message' => 'Media updated successfully',
'media' => $media->load(['image', 'category' , 'tags']), 'media' => $media->load(['image', 'categories', 'subCategories', 'tags']),
]); ]);
} }
@@ -546,7 +564,7 @@ public function toggleSaveMedia($id)
// GET saved // GET saved
public function saved() public function saved()
{ {
return auth()->user()->savedMedia()->with(['image','category' , 'myNote' , 'tags'])->get(); return auth()->user()->savedMedia()->with(['image','categories', 'subCategories', 'myNote' , 'tags'])->get();
} }
public function storeNote(Request $request, $mediaId) public function storeNote(Request $request, $mediaId)
@@ -82,7 +82,7 @@ public function show($id)
{ {
$category = MusicCategory::with(['image', 'playlists' => function($q) { $category = MusicCategory::with(['image', 'playlists' => function($q) {
$q->with(['image', 'musics' => function($q2) { $q->with(['image', 'musics' => function($q2) {
$q2->where('is_active', true)->with('image')->orderBy('order'); $q2->where('music.is_active', true)->with('image')->orderBy('music_playlist.order');
}])->where('is_active', true)->orderBy('order'); }])->where('is_active', true)->orderBy('order');
}])->findOrFail($id); }])->findOrFail($id);
+60 -37
View File
@@ -15,7 +15,7 @@ public function getAllMusic()
{ {
$userId = auth()->id(); $userId = auth()->id();
$music = Music::with(['image', 'playlist']) $music = Music::with(['image', 'playlists'])
->where('type', 'public') ->where('type', 'public')
->orWhere(function($query) use ($userId) { ->orWhere(function($query) use ($userId) {
$query->where('type', 'private') $query->where('type', 'private')
@@ -33,7 +33,7 @@ public function index()
{ {
$userId = auth()->id(); $userId = auth()->id();
$music = Music::with(['image', 'playlist']) $music = Music::with(['image', 'playlists'])
->where('type', 'public') ->where('type', 'public')
->orWhere(function($query) use ($userId) { ->orWhere(function($query) use ($userId) {
$query->where('type', 'private') $query->where('type', 'private')
@@ -51,19 +51,19 @@ public function index()
public function getMusicByPlaylist($playlistId) public function getMusicByPlaylist($playlistId)
{ {
$playlist = MusicPlaylist::findOrFail($playlistId); $playlist = MusicPlaylist::findOrFail($playlistId);
$music = Music::where('playlist_id', $playlistId) $music = $playlist->musics()
->where('is_active', true) ->where('music.is_active', true)
->with(['image', 'tags']) ->with(['image', 'tags'])
->orderBy('order') ->orderBy('music_playlist.order')
->get(); ->get();
return response()->json([ return response()->json([
'playlist' => $playlist->load('image'), 'playlist' => $playlist->load('image'),
'musics' => $music 'musics' => $music
]); ]);
} }
public function addToPlaylist(Request $request, $musicId) public function addToPlaylist(Request $request, $musicId)
{ {
$music = Music::where('id', $musicId) $music = Music::where('id', $musicId)
@@ -72,48 +72,57 @@ public function addToPlaylist(Request $request, $musicId)
->orWhere('user_id', auth()->id()); ->orWhere('user_id', auth()->id());
}) })
->firstOrFail(); ->firstOrFail();
$data = $request->validate([ $data = $request->validate([
'playlist_id' => 'required|exists:music_playlists,id', 'playlist_id' => 'required|exists:music_playlists,id',
'order' => 'nullable|integer', 'order' => 'nullable|integer',
]); ]);
$music->update([ $order = $data['order'] ?? $this->getNextOrderInPlaylist($data['playlist_id']);
'playlist_id' => $data['playlist_id'],
'order' => $data['order'] ?? $music->order, // Add (or update its order) without removing the music from other playlists.
$music->playlists()->syncWithoutDetaching([
$data['playlist_id'] => ['order' => $order],
]); ]);
return response()->json([ return response()->json([
'message' => 'Music added to playlist successfully', 'message' => 'Music added to playlist successfully',
'music' => $music->load(['image', 'playlist']) 'music' => $music->load(['image', 'playlists'])
]); ]);
} }
public function removeFromPlaylist($musicId) public function removeFromPlaylist(Request $request, $musicId)
{ {
$music = Music::where('id', $musicId) $music = Music::where('id', $musicId)
->where('user_id', auth()->id()) ->where('user_id', auth()->id())
->firstOrFail(); ->firstOrFail();
$music->update(['playlist_id' => null]); $data = $request->validate([
'playlist_id' => 'required|exists:music_playlists,id',
]);
$music->playlists()->detach($data['playlist_id']);
return response()->json(['message' => 'Music removed from playlist']); return response()->json(['message' => 'Music removed from playlist']);
} }
public function updateOrder(Request $request) public function updateOrder(Request $request)
{ {
$data = $request->validate([ $data = $request->validate([
'playlist_id' => 'required|exists:music_playlists,id',
'musics' => 'required|array', 'musics' => 'required|array',
'musics.*.id' => 'required|exists:music,id', 'musics.*.id' => 'required|exists:music,id',
'musics.*.order' => 'required|integer', 'musics.*.order' => 'required|integer',
]); ]);
$playlist = MusicPlaylist::findOrFail($data['playlist_id']);
foreach ($data['musics'] as $item) { foreach ($data['musics'] as $item) {
Music::where('id', $item['id']) $playlist->musics()->updateExistingPivot($item['id'], [
->where('user_id', auth()->id()) 'order' => $item['order'],
->update(['order' => $item['order']]); ]);
} }
return response()->json(['message' => 'Order updated successfully']); return response()->json(['message' => 'Order updated successfully']);
} }
// ✅ Upload music // ✅ Upload music
@@ -126,7 +135,9 @@ public function store(Request $request)
'file' => 'required|mimes:mp3,wav,ogg,flac|max:20971520', 'file' => 'required|mimes:mp3,wav,ogg,flac|max:20971520',
'type' => 'nullable|in:public,private', 'type' => 'nullable|in:public,private',
'image_id' => 'nullable|exists:images,id', 'image_id' => 'nullable|exists:images,id',
'playlist_id' => 'nullable|exists:music_playlists,id', 'playlist_id' => 'nullable|exists:music_playlists,id', // single (backward compatible)
'playlist_ids' => 'nullable|array', // multiple
'playlist_ids.*' => 'integer|exists:music_playlists,id',
'duration' => 'nullable|integer|min:1', // validates mm:ss or hh:mm:ss 'duration' => 'nullable|integer|min:1', // validates mm:ss or hh:mm:ss
]); ]);
@@ -161,15 +172,26 @@ public function store(Request $request)
'file_path' => $path, 'file_path' => $path,
'type' => $data['type'] ?? 'private', 'type' => $data['type'] ?? 'private',
'image_id' => $data['image_id'] ?? null, 'image_id' => $data['image_id'] ?? null,
'playlist_id' => $data['playlist_id'] ?? null,
'duration' => $data['duration'] ?? null, // Store as string 'duration' => $data['duration'] ?? null, // Store as string
'order' => $this->getNextOrderInPlaylist($data['playlist_id'] ?? null),
'is_active' => true, 'is_active' => true,
]); ]);
// Merge single + multiple playlist inputs into a unique list.
$playlistIds = collect($data['playlist_ids'] ?? [])
->push($data['playlist_id'] ?? null)
->filter()
->unique()
->values();
foreach ($playlistIds as $playlistId) {
$music->playlists()->syncWithoutDetaching([
$playlistId => ['order' => $this->getNextOrderInPlaylist($playlistId)],
]);
}
return response()->json([ return response()->json([
'message' => 'Music uploaded successfully', 'message' => 'Music uploaded successfully',
'music' => $music->load(['image', 'playlist', 'tags']), 'music' => $music->load(['image', 'playlists', 'tags']),
'url' => asset('storage/' . $path), 'url' => asset('storage/' . $path),
], 201); ], 201);
@@ -192,8 +214,11 @@ private function getNextOrderInPlaylist($playlistId)
if (!$playlistId) { if (!$playlistId) {
return 0; return 0;
} }
$maxOrder = Music::where('playlist_id', $playlistId)->max('order'); $maxOrder = \DB::table('music_playlist')
->where('playlist_id', $playlistId)
->max('order');
return ($maxOrder ?? -1) + 1; return ($maxOrder ?? -1) + 1;
} }
@@ -209,9 +234,7 @@ public function update(Request $request, $id)
'type' => 'nullable|in:public,private', 'type' => 'nullable|in:public,private',
'file' => 'nullable|mimes:mp3,wav,ogg,flac|max:20971520', 'file' => 'nullable|mimes:mp3,wav,ogg,flac|max:20971520',
'image_id' => 'nullable|exists:images,id', 'image_id' => 'nullable|exists:images,id',
'playlist_id' => 'nullable|exists:music_playlists,id',
'duration' => 'nullable|integer|min:1', 'duration' => 'nullable|integer|min:1',
'order' => 'nullable|integer',
'is_active' => 'nullable|boolean', 'is_active' => 'nullable|boolean',
]); ]);
@@ -228,7 +251,7 @@ public function update(Request $request, $id)
return response()->json([ return response()->json([
'message' => 'Music updated successfully', 'message' => 'Music updated successfully',
'music' => $music->load(['image', 'playlist', 'tags']), 'music' => $music->load(['image', 'playlists', 'tags']),
'url' => asset('storage/' . $music->file_path), 'url' => asset('storage/' . $music->file_path),
]); ]);
@@ -247,8 +270,8 @@ public function show($id)
$userId = auth()->id(); $userId = auth()->id();
$music = Music::with([ $music = Music::with([
'image', 'image',
'playlist', 'playlists',
'tags', 'tags',
'comments' => function($query) { 'comments' => function($query) {
$query->with('user')->latest()->limit(10); $query->with('user')->latest()->limit(10);
@@ -8,89 +8,138 @@
class MusicPlaylistController extends Controller class MusicPlaylistController extends Controller
{ {
public function index(Request $request) public function index(Request $request, $categoryId = null)
{ {
$query = MusicPlaylist::with(['category', 'subcategory', 'image']); $query = MusicPlaylist::with(['categories', 'subcategories', 'image']);
if ($request->has('category_id')) { $categoryId = $categoryId ?? $request->input('category_id');
$query->where('category_id', $request->category_id)->whereNull('subcategory_id');
if ($categoryId) {
$query->whereHas('categories', function ($q) use ($categoryId) {
$q->where('music_categories.id', $categoryId);
});
} }
if ($request->has('subcategory_id')) { if ($request->filled('subcategory_id')) {
$query->where('subcategory_id', $request->subcategory_id); $subcategoryId = $request->input('subcategory_id');
$query->whereHas('subcategories', function ($q) use ($subcategoryId) {
$q->where('music_subcategories.id', $subcategoryId);
});
} }
$playlists = $query->where('is_active', true)->orderBy('order')->get(); $playlists = $query->where('is_active', true)->orderBy('order')->get();
return response()->json($playlists); return response()->json($playlists);
} }
public function store(Request $request) public function store(Request $request)
{ {
$data = $request->validate([ $data = $request->validate([
'category_id' => 'nullable|exists:music_categories,id', 'category_ids' => 'nullable|array',
'subcategory_id' => 'nullable|exists:music_subcategories,id', 'category_ids.*' => 'integer|exists:music_categories,id',
'subcategory_ids' => 'nullable|array',
'subcategory_ids.*' => 'integer|exists:music_subcategories,id',
'name' => 'required|string|max:255', 'name' => 'required|string|max:255',
'description' => 'nullable|string', 'description' => 'nullable|string',
'image_id' => 'nullable|exists:images,id', 'image_id' => 'nullable|exists:images,id',
'order' => 'nullable|integer', 'order' => 'nullable|integer',
'is_active' => 'nullable|boolean', 'is_active' => 'nullable|boolean',
]); ]);
// Ensure either category_id or subcategory_id is provided // Ensure at least one category or subcategory is provided
if (!$data['category_id'] && !$data['subcategory_id']) { if (empty($data['category_ids']) && empty($data['subcategory_ids'])) {
return response()->json([ return response()->json([
'message' => 'Either category_id or subcategory_id is required' 'message' => 'At least one category or subcategory is required'
], 422); ], 422);
} }
$data['slug'] = Str::slug($data['name']); $data['slug'] = Str::slug($data['name']);
$playlist = MusicPlaylist::create($data); $playlist = MusicPlaylist::create($data);
$playlist->categories()->sync($data['category_ids'] ?? []);
$playlist->subcategories()->sync($data['subcategory_ids'] ?? []);
return response()->json([ return response()->json([
'message' => 'Playlist created successfully', 'message' => 'Playlist created successfully',
'playlist' => $playlist->load(['category', 'subcategory', 'image']) 'playlist' => $playlist->load(['categories', 'subcategories', 'image'])
], 201); ], 201);
} }
public function show($id) public function show($id)
{ {
$playlist = MusicPlaylist::with([ $playlist = MusicPlaylist::with([
'category', 'categories',
'image', 'subcategories',
'musics' => function($q) { 'image',
$q->where('is_active', true) 'musics' => function($q) {
->with(['image', 'tags']) $q->where('music.is_active', true)
->orderBy('order'); ->with(['image', 'tags'])
} ->orderBy('music_playlist.order');
])->findOrFail($id); },
'comments' => function($q) { // Add comments relationship
return response()->json($playlist); $q->with('user')->latest()->limit(10);
} }
])->findOrFail($id);
$userComment = $playlist->userComment();
$comments = $playlist->comments()
->with('user')
->latest()
->paginate(15);
return response()->json([
'playlist' => $playlist,
'statistics' => [
'total_musics' => $playlist->musics->count(),
'total_duration' => $playlist->total_duration,
'total_comments' => $playlist->comments_count,
'total_likes' => $playlist->likes_count,
'total_saves' => $playlist->saved_count,
],
'user_interaction' => [
'has_commented' => $playlist->has_user_commented,
'user_comment' => $playlist->user_comment,
'user_comment_id' => $playlist->user_comment_id,
'has_liked' => $playlist->is_liked,
'has_saved' => $playlist->is_saved,
],
'comments' => $comments,
]);
}
public function update(Request $request, $id) public function update(Request $request, $id)
{ {
$playlist = MusicPlaylist::findOrFail($id); $playlist = MusicPlaylist::findOrFail($id);
$data = $request->validate([ $data = $request->validate([
'category_id' => 'sometimes|exists:music_categories,id', 'category_ids' => 'sometimes|array',
'category_ids.*' => 'integer|exists:music_categories,id',
'subcategory_ids' => 'sometimes|array',
'subcategory_ids.*' => 'integer|exists:music_subcategories,id',
'name' => 'sometimes|string|max:255', 'name' => 'sometimes|string|max:255',
'description' => 'nullable|string', 'description' => 'nullable|string',
'image_id' => 'nullable|exists:images,id', 'image_id' => 'nullable|exists:images,id',
'order' => 'nullable|integer', 'order' => 'nullable|integer',
'is_active' => 'nullable|boolean', 'is_active' => 'nullable|boolean',
]); ]);
if (isset($data['name'])) { if (isset($data['name'])) {
$data['slug'] = Str::slug($data['name']); $data['slug'] = Str::slug($data['name']);
} }
$playlist->update($data); $playlist->update($data);
if (array_key_exists('category_ids', $data)) {
$playlist->categories()->sync($data['category_ids'] ?? []);
}
if (array_key_exists('subcategory_ids', $data)) {
$playlist->subcategories()->sync($data['subcategory_ids'] ?? []);
}
return response()->json([ return response()->json([
'message' => 'Playlist updated successfully', 'message' => 'Playlist updated successfully',
'playlist' => $playlist->load(['category', 'image']) 'playlist' => $playlist->load(['categories', 'subcategories', 'image'])
]); ]);
} }
@@ -74,7 +74,7 @@ public function show($id)
'image', 'image',
'playlists' => function($q) { 'playlists' => function($q) {
$q->with(['image', 'musics' => function($q2) { $q->with(['image', 'musics' => function($q2) {
$q2->where('is_active', true)->with('image')->orderBy('order'); $q2->where('music.is_active', true)->with('image')->orderBy('music_playlist.order');
}])->where('is_active', true)->orderBy('order'); }])->where('is_active', true)->orderBy('order');
} }
])->findOrFail($id); ])->findOrFail($id);
+7 -5
View File
@@ -13,7 +13,7 @@ class SaveController extends Controller
public function save(Request $request) public function save(Request $request)
{ {
$request->validate([ $request->validate([
'type' => 'required|string|in:music,media,breathing-template', 'type' => 'required|string|in:music,media,breathing-template,playlist',
'id' => 'required|integer', 'id' => 'required|integer',
]); ]);
@@ -40,7 +40,7 @@ public function save(Request $request)
public function unsave(Request $request) public function unsave(Request $request)
{ {
$request->validate([ $request->validate([
'type' => 'required|string|in:music,media,breathing-template', 'type' => 'required|string|in:music,media,breathing-template,playlist',
'id' => 'required|integer', 'id' => 'required|integer',
]); ]);
@@ -67,7 +67,7 @@ public function unsave(Request $request)
public function toggleSave(Request $request) public function toggleSave(Request $request)
{ {
$request->validate([ $request->validate([
'type' => 'required|string|in:music,media,breathing-template', 'type' => 'required|string|in:music,media,breathing-template,playlist',
'id' => 'required|integer', 'id' => 'required|integer',
]); ]);
@@ -112,6 +112,7 @@ public function mySavedItems(Request $request)
'music' => 'App\\Models\\Music', 'music' => 'App\\Models\\Music',
'media' => 'App\\Models\\Media', 'media' => 'App\\Models\\Media',
'breathing-template' => 'App\\Models\\BreathingTemplate', 'breathing-template' => 'App\\Models\\BreathingTemplate',
'playlist' => 'App\\Models\\MusicPlaylist',
] ]
]); ]);
} }
@@ -121,7 +122,7 @@ public function mySavedItems(Request $request)
public function checkSaved(Request $request) public function checkSaved(Request $request)
{ {
$request->validate([ $request->validate([
'type' => 'required|string|in:music,media,breathing-template', 'type' => 'required|string|in:music,media,breathing-template,playlist',
'id' => 'required|integer', 'id' => 'required|integer',
]); ]);
@@ -151,8 +152,9 @@ private function getModelClass($type)
'music' => \App\Models\Music::class, 'music' => \App\Models\Music::class,
'media' => \App\Models\Media::class, 'media' => \App\Models\Media::class,
'breathing-template' => \App\Models\BreathingTemplate::class, 'breathing-template' => \App\Models\BreathingTemplate::class,
'playlist' => \App\Models\MusicPlaylist::class,
]; ];
return $models[$type] ?? null; return $models[$type] ?? null;
} }
} }
@@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers;
use App\Models\SubCategory;
use Illuminate\Http\Request;
class SubCategoryController extends Controller
{
public function index(Request $request, $categoryId = null)
{
$query = SubCategory::with('category')->withCount('media');
$categoryId = $categoryId ?? $request->input('category_id');
if ($categoryId) {
$query->where('category_id', $categoryId);
}
return response()->json(
$query->orderBy('name')->get()
);
}
public function store(Request $request)
{
$data = $request->validate([
'category_id' => 'required|exists:categories,id',
'name' => 'required|string|max:255',
]);
$existing = SubCategory::where('category_id', $data['category_id'])
->where('name', $data['name'])
->first();
if ($existing) {
return response()->json([
'message' => 'A subcategory with this name already exists in this category',
'errors' => ['name' => ['The subcategory name must be unique within this category.']],
], 422);
}
$subCategory = SubCategory::create($data);
return response()->json([
'message' => 'Subcategory created successfully',
'sub_category' => $subCategory->load('category'),
], 201);
}
public function show($id)
{
$subCategory = SubCategory::with('category')->withCount('media')->findOrFail($id);
return response()->json($subCategory);
}
public function update(Request $request, $id)
{
$subCategory = SubCategory::findOrFail($id);
$data = $request->validate([
'category_id' => 'sometimes|exists:categories,id',
'name' => 'sometimes|string|max:255',
]);
$categoryId = $data['category_id'] ?? $subCategory->category_id;
$name = $data['name'] ?? $subCategory->name;
$existing = SubCategory::where('category_id', $categoryId)
->where('name', $name)
->where('id', '!=', $subCategory->id)
->first();
if ($existing) {
return response()->json([
'message' => 'A subcategory with this name already exists in this category',
'errors' => ['name' => ['The subcategory name must be unique within this category.']],
], 422);
}
$subCategory->update($data);
return response()->json([
'message' => 'Subcategory updated successfully',
'sub_category' => $subCategory->load('category'),
]);
}
public function destroy($id)
{
$subCategory = SubCategory::findOrFail($id);
$subCategory->delete();
return response()->json(['message' => 'Subcategory deleted successfully']);
}
}
@@ -0,0 +1,249 @@
<?php
namespace App\Http\Controllers;
use App\Models\SurveyQuestion;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class SurveyQuestionController extends Controller
{
// USER: list active questions with their options and the current user's own answers.
public function index(Request $request)
{
$questions = SurveyQuestion::with(['options', 'userAnswers'])
->where('is_active', true)
->orderBy('order')
->get();
return response()->json($questions);
}
// USER: a single question with its options and the current user's own answers.
public function show($id)
{
$question = SurveyQuestion::with(['options', 'userAnswers'])->findOrFail($id);
return response()->json($question);
}
// ADMIN: list every question (incl. inactive) with options (+ vote tallies) and all answers.
public function adminIndex(Request $request)
{
$questions = SurveyQuestion::with([
'options' => fn ($q) => $q->withCount('answers'),
'answers' => fn ($q) => $q->with(['user', 'option']),
])
->orderBy('order')
->get();
return response()->json($questions);
}
// ADMIN: a single question with options (+ vote tallies) and all users' answers.
public function adminShow($id)
{
$question = SurveyQuestion::with([
'options' => fn ($q) => $q->withCount('answers'),
'answers' => fn ($q) => $q->with(['user', 'option']),
])
->findOrFail($id);
return response()->json($question);
}
// ADMIN: clean aggregated results — per option vote counts & percentages, no raw rows.
// Pass an $id for one question, omit it for all questions.
public function adminAnalytics($id = null)
{
$query = SurveyQuestion::with(['options' => fn ($q) => $q->withCount('answers')]);
if (!is_null($id)) {
$query->where('id', $id);
}
$questions = $query->orderBy('order')->get();
if (!is_null($id) && $questions->isEmpty()) {
abort(404);
}
$analytics = $questions->map(function (SurveyQuestion $question) {
// Respondents = distinct users who answered (not number of selections).
$respondents = $question->answers()->distinct('user_id')->count('user_id');
$totalSelections = (int) $question->options->sum('answers_count');
return [
'id' => $question->id,
'question' => $question->question,
'description' => $question->description,
'type' => $question->type,
'is_active' => $question->is_active,
'total_respondents' => $respondents,
'total_selections' => $totalSelections,
'options' => $question->options->map(function ($option) use ($respondents) {
$votes = (int) $option->answers_count;
return [
'id' => $option->id,
'label' => $option->label,
'value' => $option->value,
'votes' => $votes,
// % of respondents who picked this option (can exceed 100% summed for multi-select).
'percentage' => $respondents > 0 ? round($votes / $respondents * 100, 1) : 0,
];
})->values(),
];
});
return response()->json(is_null($id) ? $analytics->values() : $analytics->first());
}
// CREATE a question together with its options.
public function store(Request $request)
{
$data = $request->validate([
'question' => 'required|string|max:255',
'description' => 'nullable|string',
'type' => 'required|in:single,multiple',
'order' => 'nullable|integer',
'is_active' => 'nullable|boolean',
'options' => 'required|array|min:1',
'options.*.label' => 'required|string|max:255',
'options.*.value' => 'nullable|string|max:255',
'options.*.order' => 'nullable|integer',
]);
$question = DB::transaction(function () use ($data) {
$question = SurveyQuestion::create([
'question' => $data['question'],
'description' => $data['description'] ?? null,
'type' => $data['type'],
'order' => $data['order'] ?? 0,
'is_active' => $data['is_active'] ?? true,
]);
$this->syncOptions($question, $data['options']);
return $question;
});
return response()->json([
'message' => 'Question created successfully',
'question' => $question->load('options'),
], 201);
}
// UPDATE a question; if options are provided they replace the existing set.
public function update(Request $request, $id)
{
$question = SurveyQuestion::findOrFail($id);
$data = $request->validate([
'question' => 'sometimes|string|max:255',
'description' => 'nullable|string',
'type' => 'sometimes|in:single,multiple',
'order' => 'nullable|integer',
'is_active' => 'nullable|boolean',
'options' => 'sometimes|array|min:1',
'options.*.label' => 'required_with:options|string|max:255',
'options.*.value' => 'nullable|string|max:255',
'options.*.order' => 'nullable|integer',
]);
DB::transaction(function () use ($question, $data) {
$question->update(array_filter(
[
'question' => $data['question'] ?? null,
'description' => array_key_exists('description', $data) ? $data['description'] : null,
'type' => $data['type'] ?? null,
'order' => $data['order'] ?? null,
'is_active' => $data['is_active'] ?? null,
],
fn ($value) => !is_null($value)
));
if (array_key_exists('options', $data)) {
// Replacing options invalidates existing answers for this question.
$question->answers()->delete();
$question->options()->delete();
$this->syncOptions($question, $data['options']);
}
});
return response()->json([
'message' => 'Question updated successfully',
'question' => $question->load('options'),
]);
}
public function destroy($id)
{
$question = SurveyQuestion::findOrFail($id);
$question->delete(); // options + answers cascade
return response()->json(['message' => 'Question deleted successfully']);
}
// USER submits their answer(s) for a question.
public function answer(Request $request, $id)
{
$question = SurveyQuestion::findOrFail($id);
$data = $request->validate([
'option_ids' => 'required|array|min:1',
'option_ids.*' => 'integer',
]);
$optionIds = array_values(array_unique($data['option_ids']));
// Every selected option must belong to this question.
$validOptionIds = $question->options()->pluck('id')->all();
if (array_diff($optionIds, $validOptionIds)) {
throw ValidationException::withMessages([
'option_ids' => ['One or more options do not belong to this question.'],
]);
}
// Enforce single vs multiple selection.
if ($question->type === 'single' && count($optionIds) > 1) {
throw ValidationException::withMessages([
'option_ids' => ['This question allows only a single option.'],
]);
}
$userId = auth()->id();
DB::transaction(function () use ($question, $optionIds, $userId) {
// Replace any previous answer for this user + question.
$question->answers()->where('user_id', $userId)->delete();
$rows = array_map(fn ($optionId) => [
'user_id' => $userId,
'survey_question_id' => $question->id,
'survey_option_id' => $optionId,
'created_at' => now(),
'updated_at' => now(),
], $optionIds);
$question->answers()->getRelated()->insert($rows);
});
return response()->json([
'message' => 'Answer submitted successfully',
'question' => $question->load(['options', 'userAnswers']),
]);
}
private function syncOptions(SurveyQuestion $question, array $options): void
{
foreach (array_values($options) as $i => $option) {
$question->options()->create([
'label' => $option['label'],
'value' => $option['value'] ?? null,
'order' => $option['order'] ?? $i,
]);
}
}
}
+10
View File
@@ -12,5 +12,15 @@ public function questions()
{ {
return $this->hasMany(Question::class); return $this->hasMany(Question::class);
} }
public function subcategories()
{
return $this->hasMany(SubCategory::class);
}
public function media()
{
return $this->belongsToMany(Media::class, 'category_media');
}
} }
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Like extends Model
{
protected $table = 'likes';
protected $fillable = [
'user_id',
'likeable_id',
'likeable_type',
];
public function user(): BelongsTo
{
return $this->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();
}
}
+14 -6
View File
@@ -6,14 +6,13 @@
use App\Traits\HasRatings; use App\Traits\HasRatings;
use App\Traits\HasComments; use App\Traits\HasComments;
use App\Traits\HasSaves; use App\Traits\HasSaves;
use App\Traits\HasLikes;
class Media extends Model class Media extends Model
{ {
use HasRatings, HasComments,HasSaves; use HasRatings, HasComments,HasSaves,HasLikes;
protected $fillable = [ protected $fillable = [
'user_id', 'user_id',
'image_id', 'image_id',
'category_id',
'title', 'title',
'caption', 'caption',
'type', 'type',
@@ -33,16 +32,25 @@ class Media extends Model
'user_comment_id', 'user_comment_id',
'has_user_rated', 'has_user_rated',
'is_saved', 'is_saved',
'saved_count' 'saved_count',
'is_liked',
'likes_count',
'is_liked',
'likes_count',
]; ];
public function image() public function image()
{ {
return $this->belongsTo(Image::class); return $this->belongsTo(Image::class);
} }
public function category() public function categories()
{ {
return $this->belongsTo(Category::class); return $this->belongsToMany(Category::class, 'category_media');
}
public function subCategories()
{
return $this->belongsToMany(SubCategory::class, 'media_sub_category');
} }
public function tags() public function tags()
{ {
+11 -7
View File
@@ -11,20 +11,20 @@
use App\Traits\HasRatings; use App\Traits\HasRatings;
use App\Traits\HasComments; use App\Traits\HasComments;
use App\Traits\HasSaves; use App\Traits\HasSaves;
use App\Traits\HasLikes;
class Music extends Model class Music extends Model
{ {
use HasFactory, HasRatings, HasComments , HasSaves; use HasFactory, HasRatings, HasComments , HasSaves,HasLikes;
protected $table = 'music'; protected $table = 'music';
protected $fillable = [ protected $fillable = [
'user_id', 'title', 'artist', 'file_path', 'type', 'user_id', 'title', 'artist', 'file_path', 'type',
'playlist_id', 'image_id', 'duration', 'order', 'is_active' 'image_id', 'duration', 'is_active'
]; ];
protected $casts = [ protected $casts = [
'duration' => 'integer', 'duration' => 'integer',
'order' => 'integer',
'is_active' => 'boolean', 'is_active' => 'boolean',
]; ];
protected $attributes = [ protected $attributes = [
@@ -35,9 +35,11 @@ public function user()
{ {
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
public function playlist(): BelongsTo public function playlists(): BelongsToMany
{ {
return $this->belongsTo(MusicPlaylist::class, 'playlist_id'); return $this->belongsToMany(MusicPlaylist::class, 'music_playlist', 'music_id', 'playlist_id')
->withPivot('order')
->withTimestamps();
} }
public function tags(): BelongsToMany public function tags(): BelongsToMany
{ {
@@ -54,7 +56,9 @@ public function tags(): BelongsToMany
'user_comment_id', 'user_comment_id',
'has_user_rated' , 'has_user_rated' ,
'is_saved', 'is_saved',
'saved_count' 'saved_count',
'is_liked',
'likes_count'
]; ];
public function getUrlAttribute() public function getUrlAttribute()
+4 -3
View File
@@ -4,6 +4,7 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; // ← Add this use Illuminate\Database\Eloquent\Relations\BelongsTo; // ← Add this
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; // ← Add this use Illuminate\Database\Eloquent\Relations\HasMany; // ← Add this
class MusicCategory extends Model class MusicCategory extends Model
{ {
@@ -18,9 +19,9 @@ class MusicCategory extends Model
'order' => 'integer', 'order' => 'integer',
]; ];
public function playlists(): HasMany public function playlists(): BelongsToMany
{ {
return $this->hasMany(MusicPlaylist::class, 'category_id'); return $this->belongsToMany(MusicPlaylist::class, 'music_category_playlist', 'category_id', 'playlist_id');
} }
public function subcategories(): HasMany public function subcategories(): HasMany
@@ -45,7 +46,7 @@ public function image(): BelongsTo
public function getActivePlaylistsAttribute() public function getActivePlaylistsAttribute()
{ {
return $this->playlists()->where('is_active', true)->get(); return $this->playlists()->where('music_playlists.is_active', true)->get();
} }
// Total music count across all playlists and subcategories // Total music count across all playlists and subcategories
+28 -12
View File
@@ -4,34 +4,50 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; // ← Add this use Illuminate\Database\Eloquent\Relations\BelongsTo; // ← Add this
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; // ← Add this use Illuminate\Database\Eloquent\Relations\HasMany; // ← Add this
use App\Traits\HasComments; // Add this
use App\Traits\HasLikes;
use App\Traits\HasSaves;
class MusicPlaylist extends Model class MusicPlaylist extends Model
{ {
use HasComments, HasLikes, HasSaves;
protected $table = 'music_playlists'; protected $table = 'music_playlists';
protected $fillable = [ protected $fillable = [
'category_id', 'subcategory_id','name', 'slug', 'description', 'image_id', 'order', 'is_active' 'name', 'slug', 'description', 'image_id', 'order', 'is_active'
]; ];
protected $casts = [ protected $casts = [
'is_active' => 'boolean', 'is_active' => 'boolean',
'order' => 'integer', 'order' => 'integer',
]; ];
protected $appends = [
public function category(): BelongsTo 'comments_count',
'has_user_commented',
'user_comment',
'user_comment_id',
'is_liked',
'likes_count',
'is_saved',
'saved_count'
];
public function categories(): BelongsToMany
{ {
return $this->belongsTo(MusicCategory::class, 'category_id'); return $this->belongsToMany(MusicCategory::class, 'music_category_playlist', 'playlist_id', 'category_id');
} }
public function subcategory(): BelongsTo public function subcategories(): BelongsToMany
{ {
return $this->belongsTo(MusicSubcategory::class, 'subcategory_id'); return $this->belongsToMany(MusicSubcategory::class, 'music_subcategory_playlist', 'playlist_id', 'subcategory_id');
} }
public function musics(): HasMany public function musics(): BelongsToMany
{ {
return $this->hasMany(Music::class, 'playlist_id'); return $this->belongsToMany(Music::class, 'music_playlist', 'playlist_id', 'music_id')
->withPivot('order')
->withTimestamps();
} }
public function image(): BelongsTo public function image(): BelongsTo
@@ -41,11 +57,11 @@ public function image(): BelongsTo
public function getActiveMusicsAttribute() public function getActiveMusicsAttribute()
{ {
return $this->musics()->where('is_active', true)->orderBy('order')->get(); return $this->musics()->where('music.is_active', true)->orderBy('music_playlist.order')->get();
} }
public function getTotalDurationAttribute() public function getTotalDurationAttribute()
{ {
return $this->musics()->where('is_active', true)->sum('duration'); return $this->musics()->where('music.is_active', true)->sum('music.duration');
} }
} }
+4 -3
View File
@@ -4,6 +4,7 @@
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
class MusicSubcategory extends Model class MusicSubcategory extends Model
{ {
@@ -23,9 +24,9 @@ public function category(): BelongsTo
return $this->belongsTo(MusicCategory::class, 'category_id'); return $this->belongsTo(MusicCategory::class, 'category_id');
} }
public function playlists(): HasMany public function playlists(): BelongsToMany
{ {
return $this->hasMany(MusicPlaylist::class, 'subcategory_id'); return $this->belongsToMany(MusicPlaylist::class, 'music_subcategory_playlist', 'subcategory_id', 'playlist_id');
} }
public function image(): BelongsTo public function image(): BelongsTo
@@ -35,7 +36,7 @@ public function image(): BelongsTo
public function getActivePlaylistsAttribute() public function getActivePlaylistsAttribute()
{ {
return $this->playlists()->where('is_active', true)->orderBy('order')->get(); return $this->playlists()->where('music_playlists.is_active', true)->orderBy('music_playlists.order')->get();
} }
// Get total music count across all playlists in this subcategory // Get total music count across all playlists in this subcategory
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class SubCategory extends Model
{
protected $table = 'sub_categories';
protected $fillable = ['category_id', 'name'];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function media(): BelongsToMany
{
return $this->belongsToMany(Media::class, 'media_sub_category');
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class SurveyAnswer extends Model
{
protected $fillable = ['user_id', 'survey_question_id', 'survey_option_id'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function question(): BelongsTo
{
return $this->belongsTo(SurveyQuestion::class, 'survey_question_id');
}
public function option(): BelongsTo
{
return $this->belongsTo(SurveyOption::class, 'survey_option_id');
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class SurveyOption extends Model
{
protected $fillable = ['survey_question_id', 'label', 'value', 'order'];
protected $casts = [
'order' => 'integer',
];
public function question(): BelongsTo
{
return $this->belongsTo(SurveyQuestion::class, 'survey_question_id');
}
public function answers(): HasMany
{
return $this->hasMany(SurveyAnswer::class);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class SurveyQuestion extends Model
{
protected $fillable = ['question', 'description', 'type', 'order', 'is_active'];
protected $casts = [
'is_active' => 'boolean',
'order' => 'integer',
];
public function options(): HasMany
{
return $this->hasMany(SurveyOption::class)->orderBy('order');
}
public function answers(): HasMany
{
return $this->hasMany(SurveyAnswer::class);
}
// The current user's selected option ids for this question.
public function userAnswers(): HasMany
{
return $this->hasMany(SurveyAnswer::class)->where('user_id', auth()->id());
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
// app/Traits/HasLikes.php
namespace App\Traits;
use App\Models\Like;
use Illuminate\Database\Eloquent\Relations\MorphMany;
trait HasLikes
{
public function likes(): MorphMany
{
return $this->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();
}
}
@@ -3,6 +3,7 @@
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration return new class extends Migration
{ {
@@ -11,9 +12,22 @@
*/ */
public function up(): void public function up(): void
{ {
Schema::table('music', function (Blueprint $table) { // Get the database driver
$table->integer('duration')->nullable()->change(); $driver = DB::connection()->getDriverName();
});
if ($driver === 'pgsql') {
// PostgreSQL: Use raw statement with USING clause
DB::statement('ALTER TABLE music ALTER COLUMN duration TYPE integer USING (duration::integer)');
Schema::table('music', function (Blueprint $table) {
$table->integer('duration')->nullable()->change();
});
} elseif ($driver === 'mysql') {
// MySQL: Can directly change column type
Schema::table('music', function (Blueprint $table) {
$table->integer('duration')->nullable()->change();
});
}
} }
/** /**
@@ -21,8 +35,21 @@ public function up(): void
*/ */
public function down(): void public function down(): void
{ {
Schema::table('music', function (Blueprint $table) { // Get the database driver
$table->string('duration')->nullable()->change(); $driver = DB::connection()->getDriverName();
});
if ($driver === 'pgsql') {
// PostgreSQL: Convert back to text
DB::statement('ALTER TABLE music ALTER COLUMN duration TYPE text USING (duration::text)');
Schema::table('music', function (Blueprint $table) {
$table->string('duration')->nullable()->change();
});
} elseif ($driver === 'mysql') {
// MySQL: Change back to string
Schema::table('music', function (Blueprint $table) {
$table->string('duration')->nullable()->change();
});
}
} }
}; };
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('likes', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('sub_categories', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained('categories')->cascadeOnDelete();
$table->string('name');
$table->timestamps();
$table->unique(['category_id', 'name']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sub_categories');
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('media', function (Blueprint $table) {
$table->unsignedBigInteger('subcategory_id')->nullable()->after('category_id');
$table->foreign('subcategory_id')->references('id')->on('sub_categories')->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('media', function (Blueprint $table) {
$table->dropForeign(['subcategory_id']);
$table->dropColumn('subcategory_id');
});
}
};
@@ -0,0 +1,79 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('category_media', function (Blueprint $table) {
$table->id();
$table->foreignId('media_id')->constrained('media')->cascadeOnDelete();
$table->foreignId('category_id')->constrained('categories')->cascadeOnDelete();
$table->timestamps();
$table->unique(['media_id', 'category_id']);
});
Schema::create('media_sub_category', function (Blueprint $table) {
$table->id();
$table->foreignId('media_id')->constrained('media')->cascadeOnDelete();
$table->foreignId('sub_category_id')->constrained('sub_categories')->cascadeOnDelete();
$table->timestamps();
$table->unique(['media_id', 'sub_category_id']);
});
// Backfill the new pivots from the existing single columns.
if (Schema::hasColumn('media', 'category_id')) {
DB::table('media')
->whereNotNull('category_id')
->orderBy('id')
->select('id', 'category_id')
->chunk(200, function ($rows) {
$now = now();
$insert = $rows->map(fn ($row) => [
'media_id' => $row->id,
'category_id' => $row->category_id,
'created_at' => $now,
'updated_at' => $now,
])->all();
DB::table('category_media')->insertOrIgnore($insert);
});
}
if (Schema::hasColumn('media', 'subcategory_id')) {
DB::table('media')
->whereNotNull('subcategory_id')
->orderBy('id')
->select('id', 'subcategory_id')
->chunk(200, function ($rows) {
$now = now();
$insert = $rows->map(fn ($row) => [
'media_id' => $row->id,
'sub_category_id' => $row->subcategory_id,
'created_at' => $now,
'updated_at' => $now,
])->all();
DB::table('media_sub_category')->insertOrIgnore($insert);
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('media_sub_category');
Schema::dropIfExists('category_media');
}
};
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('media', function (Blueprint $table) {
$table->dropForeign(['category_id']);
$table->dropColumn('category_id');
$table->dropForeign(['subcategory_id']);
$table->dropColumn('subcategory_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('media', function (Blueprint $table) {
$table->unsignedBigInteger('category_id')->nullable()->after('image_id');
$table->foreign('category_id')->references('id')->on('categories')->nullOnDelete();
$table->unsignedBigInteger('subcategory_id')->nullable()->after('category_id');
$table->foreign('subcategory_id')->references('id')->on('sub_categories')->nullOnDelete();
});
}
};
@@ -0,0 +1,79 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('music_category_playlist', function (Blueprint $table) {
$table->id();
$table->foreignId('playlist_id')->constrained('music_playlists')->cascadeOnDelete();
$table->foreignId('category_id')->constrained('music_categories')->cascadeOnDelete();
$table->timestamps();
$table->unique(['playlist_id', 'category_id']);
});
Schema::create('music_subcategory_playlist', function (Blueprint $table) {
$table->id();
$table->foreignId('playlist_id')->constrained('music_playlists')->cascadeOnDelete();
$table->foreignId('subcategory_id')->constrained('music_subcategories')->cascadeOnDelete();
$table->timestamps();
$table->unique(['playlist_id', 'subcategory_id']);
});
// Backfill the new pivots from the existing single columns.
if (Schema::hasColumn('music_playlists', 'category_id')) {
DB::table('music_playlists')
->whereNotNull('category_id')
->orderBy('id')
->select('id', 'category_id')
->chunk(200, function ($rows) {
$now = now();
$insert = $rows->map(fn ($row) => [
'playlist_id' => $row->id,
'category_id' => $row->category_id,
'created_at' => $now,
'updated_at' => $now,
])->all();
DB::table('music_category_playlist')->insertOrIgnore($insert);
});
}
if (Schema::hasColumn('music_playlists', 'subcategory_id')) {
DB::table('music_playlists')
->whereNotNull('subcategory_id')
->orderBy('id')
->select('id', 'subcategory_id')
->chunk(200, function ($rows) {
$now = now();
$insert = $rows->map(fn ($row) => [
'playlist_id' => $row->id,
'subcategory_id' => $row->subcategory_id,
'created_at' => $now,
'updated_at' => $now,
])->all();
DB::table('music_subcategory_playlist')->insertOrIgnore($insert);
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('music_subcategory_playlist');
Schema::dropIfExists('music_category_playlist');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('music_playlists', function (Blueprint $table) {
$table->dropForeign(['subcategory_id']);
$table->dropColumn('subcategory_id');
$table->dropForeign(['category_id']);
$table->dropColumn('category_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('music_playlists', function (Blueprint $table) {
$table->foreignId('category_id')->nullable()->after('id')
->constrained('music_categories')->nullOnDelete();
$table->foreignId('subcategory_id')->nullable()->after('category_id')
->constrained('music_subcategories')->nullOnDelete();
});
}
};
@@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('music_playlist', function (Blueprint $table) {
$table->id();
$table->foreignId('music_id')->constrained('music')->cascadeOnDelete();
$table->foreignId('playlist_id')->constrained('music_playlists')->cascadeOnDelete();
$table->integer('order')->default(0);
$table->timestamps();
$table->unique(['music_id', 'playlist_id']);
});
// Backfill the pivot from the existing single playlist_id column.
if (Schema::hasColumn('music', 'playlist_id')) {
DB::table('music')
->whereNotNull('playlist_id')
->orderBy('id')
->select('id', 'playlist_id', 'order')
->chunk(200, function ($rows) {
$now = now();
$insert = $rows->map(fn ($row) => [
'music_id' => $row->id,
'playlist_id' => $row->playlist_id,
'order' => $row->order ?? 0,
'created_at' => $now,
'updated_at' => $now,
])->all();
DB::table('music_playlist')->insertOrIgnore($insert);
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('music_playlist');
}
};
@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('music', function (Blueprint $table) {
$table->dropForeign(['playlist_id']);
$table->dropColumn('playlist_id');
$table->dropColumn('order');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('music', function (Blueprint $table) {
$table->foreignId('playlist_id')->nullable()->after('type')
->constrained('music_playlists')->nullOnDelete();
$table->integer('order')->default(0)->after('duration');
});
}
};
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('survey_questions', function (Blueprint $table) {
$table->id();
$table->string('question');
$table->text('description')->nullable();
// single = user picks exactly one option, multiple = user can pick many
$table->enum('type', ['single', 'multiple'])->default('single');
$table->integer('order')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('survey_questions');
}
};
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('survey_options', function (Blueprint $table) {
$table->id();
$table->foreignId('survey_question_id')->constrained('survey_questions')->cascadeOnDelete();
$table->string('label');
$table->string('value')->nullable(); // optional machine value
$table->integer('order')->default(0);
$table->timestamps();
$table->index(['survey_question_id', 'order']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('survey_options');
}
};
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('survey_answers', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('survey_question_id')->constrained('survey_questions')->cascadeOnDelete();
$table->foreignId('survey_option_id')->constrained('survey_options')->cascadeOnDelete();
$table->timestamps();
// A user can select a given option only once.
$table->unique(['user_id', 'survey_option_id']);
$table->index(['user_id', 'survey_question_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('survey_answers');
}
};
+36
View File
@@ -11,16 +11,20 @@
use App\Http\Controllers\WorryController; use App\Http\Controllers\WorryController;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use App\Http\Controllers\QuestionController; use App\Http\Controllers\QuestionController;
use App\Http\Controllers\SurveyQuestionController;
use App\Http\Controllers\SliderController; use App\Http\Controllers\SliderController;
use App\Http\Controllers\ImageController; use App\Http\Controllers\ImageController;
use App\Http\Controllers\MusicController; use App\Http\Controllers\MusicController;
use App\Http\Controllers\MediaController; use App\Http\Controllers\MediaController;
use App\Http\Controllers\CategoryController;
use App\Http\Controllers\SubCategoryController;
use App\Http\Controllers\MusicCategoryController; use App\Http\Controllers\MusicCategoryController;
use App\Http\Controllers\MusicPlaylistController; use App\Http\Controllers\MusicPlaylistController;
use App\Http\Controllers\RatingController; use App\Http\Controllers\RatingController;
use App\Http\Controllers\CommentController; use App\Http\Controllers\CommentController;
use App\Http\Controllers\MusicSubcategoryController; use App\Http\Controllers\MusicSubcategoryController;
use App\Http\Controllers\SaveController; use App\Http\Controllers\SaveController;
use App\Http\Controllers\LikeController;
Route::get('/test-hash', function() { Route::get('/test-hash', function() {
$plain = 'amnk1380'; $plain = 'amnk1380';
@@ -113,6 +117,20 @@
Route::delete('/questions/{question}', [QuestionController::class, 'destroy']); Route::delete('/questions/{question}', [QuestionController::class, 'destroy']);
/// survey questions feature (question + description + single/multi options, answered by users)
// Admin: see all users' answers (must be registered before the resource so it isn't caught by {survey_question}).
Route::middleware('abilities:admin')->group(function () {
// Aggregated analytics — register before the {id} routes so "analytics" isn't read as an id.
Route::get('/admin/survey-questions/analytics', [SurveyQuestionController::class, 'adminAnalytics']);
Route::get('/admin/survey-questions/{id}/analytics', [SurveyQuestionController::class, 'adminAnalytics']);
Route::get('/admin/survey-questions', [SurveyQuestionController::class, 'adminIndex']);
Route::get('/admin/survey-questions/{id}', [SurveyQuestionController::class, 'adminShow']);
});
// User: answer + read questions with their own answers only.
Route::post('/survey-questions/{id}/answer', [SurveyQuestionController::class, 'answer']);
Route::apiResource('survey-questions', SurveyQuestionController::class);
/// slider feature /// slider feature
// Route::get('/slider', [SliderController::class, 'index']); // Route::get('/slider', [SliderController::class, 'index']);
@@ -157,6 +175,13 @@
//add note to media //add note to media
Route::post('/media/{id}/note', [MediaController::class, 'storeNote']); Route::post('/media/{id}/note', [MediaController::class, 'storeNote']);
// Media categories
Route::apiResource('categories', CategoryController::class);
// Media sub categories
Route::get('sub-categories/by-category/{categoryId}', [SubCategoryController::class, 'index']);
Route::apiResource('sub-categories', SubCategoryController::class);
// Music Categories // Music Categories
@@ -211,4 +236,15 @@
Route::get('/my-saved', [SaveController::class, 'mySavedItems']); Route::get('/my-saved', [SaveController::class, 'mySavedItems']);
Route::post('/check', [SaveController::class, 'checkSaved']); 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']);
});
}); });