diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php new file mode 100644 index 0000000..82ad96a --- /dev/null +++ b/app/Http/Controllers/CategoryController.php @@ -0,0 +1,67 @@ +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']); + } +} diff --git a/app/Http/Controllers/MediaController.php b/app/Http/Controllers/MediaController.php index 93e652f..597eae1 100644 --- a/app/Http/Controllers/MediaController.php +++ b/app/Http/Controllers/MediaController.php @@ -19,7 +19,7 @@ public function store(Request $request) 'caption' => 'nullable|string', 'type' => 'required|in:audio,video', 'category_id' => 'nullable|exists:categories,id', - 'category_name' => 'nullable|string|max:255', + 'subcategory_id' => 'nullable|exists:sub_categories,id', 'image_id' => 'nullable|exists:images,id', 'duration' => 'nullable|integer', 'is_premium' => 'nullable|boolean', @@ -31,14 +31,8 @@ public function store(Request $request) 'tags.*' => 'string', ]); - $categoryId = $data['category_id'] ?? null; + $categoryId = $data['category_id'] ?? null; - if (!$categoryId && isset($data['category_name'])) { - $category = Category::firstOrCreate([ - 'name' => $data['category_name'] - ]); - $categoryId = $category->id; - } $path = null; if ($request->hasFile('file')) { $path = $request->file('file')->store('media', 'public'); @@ -53,6 +47,7 @@ public function store(Request $request) 'external_url' => $data['external_url'] ?? null, 'image_id' => $data['image_id'] ?? null, 'category_id' => $categoryId, + 'subcategory_id' => $data['subcategory_id'] ?? null, 'duration' => $data['duration'] ?? null, 'visibility' => $data['visibility'] ?? 'public', 'is_premium'=> $data['is_premium'] ?? false @@ -69,12 +64,12 @@ public function store(Request $request) } return response()->json([ 'message' => 'Media created successfully', - 'media' => $media->load(['image', 'category' , 'tags']), + 'media' => $media->load(['image', 'category', 'subCategory', 'tags']), ]); } public function index(Request $request) { - $query = Media::with(['image', 'category', 'myNote', 'tags','comments']) + $query = Media::with(['image', 'category', 'subCategory', 'myNote', 'tags','comments']) ->where(function ($q) { $q->where('visibility', 'public') ->orWhere('user_id', auth()->id()); @@ -91,6 +86,11 @@ public function index(Request $request) $query->whereIn('category_id', $categories); } + if ($request->filled('subcategories')) { + $subcategories = explode(',', $request->subcategories); + $query->whereIn('subcategory_id', $subcategories); + } + /* |-------------------------------------------------------------------------- | 2️⃣ Multi Duration Ranges @@ -322,9 +322,10 @@ public function filters(Request $request) public function show($id) { $media = Media::with([ - 'image', - 'category', - 'myNote', + 'image', + 'category', + 'subCategory', + 'myNote', 'tags', 'comments' => function($query) { $query->with('user')->latest()->limit(10); @@ -361,6 +362,7 @@ public function show($id) 'is_premium' => $media->is_premium, 'image' => $media->image, 'category' => $media->category, + 'sub_category' => $media->subCategory, 'tags' => $media->tags, 'myNote' => $media->myNote, 'is_saved' => $media->is_saved, @@ -442,9 +444,8 @@ public function update(Request $request, $id) 'caption' => 'nullable|string', 'type' => 'nullable|in:audio,video', - // support both: category_id or category_name 'category_id' => 'nullable|exists:categories,id', - 'category_name' => 'nullable|string|max:255', + 'subcategory_id' => 'nullable|exists:sub_categories,id', 'is_premium' => 'nullable|boolean', 'image_id' => 'nullable|exists:images,id', 'duration' => 'nullable|integer', @@ -455,16 +456,8 @@ public function update(Request $request, $id) '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 --- if ($request->hasFile('file')) { Storage::disk('public')->delete($media->file_path); @@ -477,6 +470,7 @@ public function update(Request $request, $id) 'caption' => $data['caption'] ?? $media->caption, 'type' => $data['type'] ?? $media->type, 'category_id' => $categoryId, + 'subcategory_id' => array_key_exists('subcategory_id', $data) ? $data['subcategory_id'] : $media->subcategory_id, 'is_premium' => $data['is_premium'] ?? $media->is_premium, 'duration' => $data['duration'] ?? $media->duration, 'external_url' => $data['external_url'] ?? $media->external_url, @@ -546,7 +540,7 @@ public function toggleSaveMedia($id) // GET saved public function saved() { - return auth()->user()->savedMedia()->with(['image','category' , 'myNote' , 'tags'])->get(); + return auth()->user()->savedMedia()->with(['image','category', 'subCategory', 'myNote' , 'tags'])->get(); } public function storeNote(Request $request, $mediaId) diff --git a/app/Http/Controllers/SubCategoryController.php b/app/Http/Controllers/SubCategoryController.php new file mode 100644 index 0000000..17e2e0d --- /dev/null +++ b/app/Http/Controllers/SubCategoryController.php @@ -0,0 +1,97 @@ +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']); + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php index 40735af..e2f5ae5 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -12,5 +12,10 @@ public function questions() { return $this->hasMany(Question::class); } + + public function subcategories() + { + return $this->hasMany(SubCategory::class); + } } diff --git a/app/Models/Media.php b/app/Models/Media.php index e57db23..b1aa826 100644 --- a/app/Models/Media.php +++ b/app/Models/Media.php @@ -14,6 +14,7 @@ class Media extends Model 'user_id', 'image_id', 'category_id', + 'subcategory_id', 'title', 'caption', 'type', @@ -48,6 +49,11 @@ public function category() { return $this->belongsTo(Category::class); } + + public function subCategory() + { + return $this->belongsTo(SubCategory::class, 'subcategory_id'); + } public function tags() { return $this->belongsToMany(Tag::class, 'media_tag'); diff --git a/app/Models/SubCategory.php b/app/Models/SubCategory.php new file mode 100644 index 0000000..e9eb0d2 --- /dev/null +++ b/app/Models/SubCategory.php @@ -0,0 +1,24 @@ +belongsTo(Category::class); + } + + public function media(): HasMany + { + return $this->hasMany(Media::class, 'subcategory_id'); + } +} diff --git a/database/migrations/2026_05_30_100000_create_sub_categories_table.php b/database/migrations/2026_05_30_100000_create_sub_categories_table.php new file mode 100644 index 0000000..1cfecee --- /dev/null +++ b/database/migrations/2026_05_30_100000_create_sub_categories_table.php @@ -0,0 +1,31 @@ +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'); + } +}; diff --git a/database/migrations/2026_05_30_100100_add_subcategory_id_to_media_table.php b/database/migrations/2026_05_30_100100_add_subcategory_id_to_media_table.php new file mode 100644 index 0000000..6c37d59 --- /dev/null +++ b/database/migrations/2026_05_30_100100_add_subcategory_id_to_media_table.php @@ -0,0 +1,31 @@ +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'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index c28a160..3936da7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -15,6 +15,8 @@ use App\Http\Controllers\ImageController; use App\Http\Controllers\MusicController; use App\Http\Controllers\MediaController; +use App\Http\Controllers\CategoryController; +use App\Http\Controllers\SubCategoryController; use App\Http\Controllers\MusicCategoryController; use App\Http\Controllers\MusicPlaylistController; use App\Http\Controllers\RatingController; @@ -158,6 +160,13 @@ //add note to media 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