diff --git a/app/Http/Controllers/SurveyQuestionController.php b/app/Http/Controllers/SurveyQuestionController.php index 1a23a8f..56ed0c8 100644 --- a/app/Http/Controllers/SurveyQuestionController.php +++ b/app/Http/Controllers/SurveyQuestionController.php @@ -297,40 +297,85 @@ public function suggestedMedia(Request $request) { $userId = auth()->id(); + $limit = (int) $request->input('limit', 20); + $limit = max(1, min($limit, 100)); + $answersQuery = SurveyAnswer::where('user_id', $userId); if ($request->filled('question_id')) { $answersQuery->where('survey_question_id', $request->question_id); } $optionIds = $answersQuery->pluck('survey_option_id'); - if ($optionIds->isEmpty()) { - return response()->json([]); + // Tags behind the user's chosen options. + $tagIds = $optionIds->isEmpty() + ? collect() + : DB::table('survey_option_tag') + ->whereIn('survey_option_id', $optionIds) + ->pluck('tag_id') + ->unique() + ->values(); + + // Visible to this user: public or their own. + $visible = function ($q) use ($userId) { + $q->where('visibility', 'public')->orWhere('user_id', $userId); + }; + + $eager = ['image', 'detailImage', 'categories', 'subCategories', 'tags']; + + $media = collect(); + + if ($tagIds->isNotEmpty()) { + // 1) Media directly sharing the chosen tags, ranked by overlap. + $tagMatched = Media::query() + ->whereHas('tags', fn ($q) => $q->whereIn('tags.id', $tagIds)) + ->withCount(['tags as match_count' => fn ($q) => $q->whereIn('tags.id', $tagIds)]) + ->where($visible) + ->with($eager) + ->orderByDesc('match_count') + ->orderByDesc('created_at') + ->get(); + + // 2) Broaden to "similar" media: same categories / sub-categories as + // the tag-matched media (so a thinly-tagged catalog still surfaces + // the rest of the topic, not just the one over-tagged item). + $categoryIds = $tagMatched->pluck('categories')->flatten(1)->pluck('id')->unique()->values(); + $subCategoryIds = $tagMatched->pluck('subCategories')->flatten(1)->pluck('id')->unique()->values(); + + $similar = collect(); + if ($categoryIds->isNotEmpty() || $subCategoryIds->isNotEmpty()) { + $similar = Media::query() + ->where($visible) + ->whereNotIn('id', $tagMatched->pluck('id')) + ->where(function ($q) use ($categoryIds, $subCategoryIds) { + if ($categoryIds->isNotEmpty()) { + $q->orWhereHas('categories', fn ($c) => $c->whereIn('categories.id', $categoryIds)); + } + if ($subCategoryIds->isNotEmpty()) { + $q->orWhereHas('subCategories', fn ($s) => $s->whereIn('sub_categories.id', $subCategoryIds)); + } + }) + ->with($eager) + ->orderByDesc('created_at') + ->get() + ->each(fn ($m) => $m->match_count = 0); + } + + $media = $tagMatched->concat($similar); } - // Collect the tags behind the chosen options. - $tagIds = DB::table('survey_option_tag') - ->whereIn('survey_option_id', $optionIds) - ->pluck('tag_id') - ->unique() - ->values(); - - if ($tagIds->isEmpty()) { - return response()->json([]); + // Fallback (no answers / no tags / nothing matched): popular public media, + // so the suggestions row is never empty. + if ($media->isEmpty()) { + $media = Media::query() + ->where($visible) + ->withCount('plays as plays_count') + ->with($eager) + ->orderByDesc('plays_count') + ->orderByDesc('created_at') + ->get(); } - // Media sharing those tags, ranked by how many of them match. - $media = Media::query() - ->whereHas('tags', fn ($q) => $q->whereIn('tags.id', $tagIds)) - ->withCount(['tags as match_count' => fn ($q) => $q->whereIn('tags.id', $tagIds)]) - ->where(function ($q) use ($userId) { - $q->where('visibility', 'public')->orWhere('user_id', $userId); - }) - ->with(['image', 'categories', 'subCategories', 'tags']) - ->orderByDesc('match_count') - ->orderByDesc('created_at') - ->get(); - - return response()->json($media); + return response()->json($media->take($limit)->values()); } private function syncOptions(SurveyQuestion $question, array $options): void