From 7b888f111ea34f703f6cbc6693e2171a920d918f Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Thu, 3 Sep 2026 10:57:02 +0330 Subject: [PATCH] feat: add sub category deep link --- app/Http/Controllers/CategoryController.php | 2 +- .../Controllers/SubCategoryController.php | 49 +++++++++++++++++++ app/Models/SubCategory.php | 19 ++++++- config/deeplinks.php | 3 ++ ...eplink_columns_to_sub_categories_table.php | 30 ++++++++++++ routes/api.php | 1 + 6 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2026_09_02_100200_add_deeplink_columns_to_sub_categories_table.php diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index de01095..2e96148 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -177,7 +177,7 @@ public function publicShow($id) public function grouped($id) { $category = Category::with(['subcategories' => function ($q) { - $q->orderBy('name')->with(['media' => function ($m) { + $q->where('is_deeplink_only', false)->orderBy('name')->with(['media' => function ($m) { // Deep-link-only media stays out of the normal category screen. $m->where('is_deeplink_only', false) ->with(['image', 'detailImage', 'tags'])->latest(); diff --git a/app/Http/Controllers/SubCategoryController.php b/app/Http/Controllers/SubCategoryController.php index 5e28dac..de3a993 100644 --- a/app/Http/Controllers/SubCategoryController.php +++ b/app/Http/Controllers/SubCategoryController.php @@ -7,12 +7,21 @@ class SubCategoryController extends Controller { + // Admin listing of deep-link (hidden) subcategories: ?deeplink_only=1 returns + // only the hidden records so the admin can manage their links. The normal + // listing hides deeplink-only subcategories. public function index(Request $request, $categoryId = null) { $query = SubCategory::with('category')->withCount(['media' => fn ($q) => $q->where('is_deeplink_only', false)]); $categoryId = $categoryId ?? $request->input('category_id'); + if ($request->boolean('deeplink_only')) { + $query->where('is_deeplink_only', true); + } else { + $query->where('is_deeplink_only', false); + } + if ($categoryId) { $query->where('category_id', $categoryId); } @@ -28,6 +37,7 @@ public function store(Request $request) 'category_id' => 'required|exists:categories,id', 'name' => 'required|string|max:255', 'description' => 'nullable|string', + 'is_deeplink_only' => 'nullable|boolean', ]); $existing = SubCategory::where('category_id', $data['category_id']) @@ -41,6 +51,8 @@ public function store(Request $request) ], 422); } + $data['is_deeplink_only'] = $data['is_deeplink_only'] ?? false; + $subCategory = SubCategory::create($data); return response()->json([ @@ -56,6 +68,38 @@ public function show($id) return response()->json($subCategory); } + // Public (no auth) deep link lookup by ID. Serves any subcategory — hidden + // (deeplink-only) ones included — so https://aramejan.com/subcategory/{id} + // resolves for every shareable subcategory. A visible subcategory only lists + // its visible media; inside a hidden one everything is shown, since the deep + // link is the only way in. + public function publicShow($id) + { + $subCategory = SubCategory::with('category')->findOrFail($id); + $includeHiddenMedia = (bool) $subCategory->is_deeplink_only; + + $mediaQuery = $subCategory->media() + ->with(['image', 'detailImage', 'tags']) + ->latest(); + + if (! $includeHiddenMedia) { + $mediaQuery->where('is_deeplink_only', false); + } + + return response()->json([ + 'sub_category' => [ + 'id' => $subCategory->id, + 'name' => $subCategory->name, + 'description' => $subCategory->description, + 'category_id' => $subCategory->category_id, + 'category' => $subCategory->category, + 'is_deeplink_only' => $subCategory->is_deeplink_only, + 'deep_link' => $subCategory->deep_link, + ], + 'medias' => $mediaQuery->get(), + ]); + } + public function update(Request $request, $id) { $subCategory = SubCategory::findOrFail($id); @@ -64,6 +108,7 @@ public function update(Request $request, $id) 'category_id' => 'sometimes|exists:categories,id', 'name' => 'sometimes|string|max:255', 'description' => 'nullable|string', + 'is_deeplink_only' => 'nullable|boolean', ]); $categoryId = $data['category_id'] ?? $subCategory->category_id; @@ -81,6 +126,10 @@ public function update(Request $request, $id) ], 422); } + if (array_key_exists('is_deeplink_only', $data)) { + $subCategory->is_deeplink_only = $data['is_deeplink_only']; + } + $subCategory->update($data); return response()->json([ diff --git a/app/Models/SubCategory.php b/app/Models/SubCategory.php index 0ee3f16..f7d890d 100644 --- a/app/Models/SubCategory.php +++ b/app/Models/SubCategory.php @@ -10,7 +10,24 @@ class SubCategory extends Model { protected $table = 'sub_categories'; - protected $fillable = ['category_id', 'name', 'description']; + protected $fillable = ['category_id', 'name', 'description', 'is_deeplink_only']; + + protected $casts = [ + 'is_deeplink_only' => 'boolean', + ]; + + protected $appends = ['deep_link']; + + public function getDeepLinkAttribute(): ?string + { + $canonical = config('deeplinks.subcategory_pattern'); + + if ($canonical) { + return rtrim(str_replace('{id}', $this->id, $canonical), '/'); + } + + return url('/subcategory/' . $this->id); + } public function category(): BelongsTo { diff --git a/config/deeplinks.php b/config/deeplinks.php index d123f19..77fa8a7 100644 --- a/config/deeplinks.php +++ b/config/deeplinks.php @@ -15,4 +15,7 @@ // Pattern used for category deep links. 'category_pattern' => env('DEEPLINK_CATEGORY_PATTERN', $base . '/category/{id}'), + + // Pattern used for subcategory deep links. + 'subcategory_pattern' => env('DEEPLINK_SUBCATEGORY_PATTERN', $base . '/subcategory/{id}'), ]; diff --git a/database/migrations/2026_09_02_100200_add_deeplink_columns_to_sub_categories_table.php b/database/migrations/2026_09_02_100200_add_deeplink_columns_to_sub_categories_table.php new file mode 100644 index 0000000..0f6873e --- /dev/null +++ b/database/migrations/2026_09_02_100200_add_deeplink_columns_to_sub_categories_table.php @@ -0,0 +1,30 @@ +boolean('is_deeplink_only')->default(false)->after('description'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('sub_categories', function (Blueprint $table) { + $table->dropColumn('is_deeplink_only'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index fbf65ec..d95cd32 100644 --- a/routes/api.php +++ b/routes/api.php @@ -57,6 +57,7 @@ // fetched without authentication when reached via their share link. Route::get('public/media/{id}', [MediaController::class, 'publicShow']); Route::get('public/categories/{id}', [CategoryController::class, 'publicShow']); +Route::get('public/sub-categories/{id}', [SubCategoryController::class, 'publicShow']); Route::prefix('auth')->controller(UserController::class)->group(function () { Route::post('login-with-approo-v2', [UserController::class, 'loginV2']);