From 2b5a7c9b1b50b1e1987f0cf8660bc7f1a00b4b10 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Thu, 25 Jun 2026 10:07:42 +0330 Subject: [PATCH 1/3] feat: add btn text --- .../Controllers/AnnouncementController.php | 2 ++ app/Http/Controllers/AppVersionController.php | 2 ++ app/Models/Announcement.php | 2 +- app/Models/AppVersion.php | 2 +- ...text_to_announcements_and_app_versions.php | 30 +++++++++++++++++++ 5 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2026_06_24_120000_add_button_text_to_announcements_and_app_versions.php diff --git a/app/Http/Controllers/AnnouncementController.php b/app/Http/Controllers/AnnouncementController.php index 6035e8f..0966959 100644 --- a/app/Http/Controllers/AnnouncementController.php +++ b/app/Http/Controllers/AnnouncementController.php @@ -36,6 +36,7 @@ public function store(Request $request) 'title' => 'required|string|max:255', 'description' => 'nullable|string', 'link' => 'nullable|string|max:500', + 'button_text' => 'nullable|string|max:255', 'start_date' => 'nullable|date', 'end_date' => 'nullable|date|after_or_equal:start_date', 'image_id' => 'nullable|exists:images,id', @@ -68,6 +69,7 @@ public function update(Request $request, $id) 'title' => 'sometimes|string|max:255', 'description' => 'nullable|string', 'link' => 'nullable|string|max:500', + 'button_text' => 'nullable|string|max:255', 'start_date' => 'nullable|date', 'end_date' => 'nullable|date|after_or_equal:start_date', 'image_id' => 'nullable|exists:images,id', diff --git a/app/Http/Controllers/AppVersionController.php b/app/Http/Controllers/AppVersionController.php index 85b7c8d..bab659a 100644 --- a/app/Http/Controllers/AppVersionController.php +++ b/app/Http/Controllers/AppVersionController.php @@ -34,6 +34,7 @@ public function store(Request $request) 'version_name' => 'required|string|max:255', 'version_code' => 'required|integer|min:0', 'link' => 'nullable|string|max:500', + 'button_text' => 'nullable|string|max:255', 'image_id' => 'nullable|exists:images,id', 'image' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192', ]); @@ -66,6 +67,7 @@ public function update(Request $request, $id) 'version_name' => 'sometimes|string|max:255', 'version_code' => 'sometimes|integer|min:0', 'link' => 'nullable|string|max:500', + 'button_text' => 'nullable|string|max:255', 'image_id' => 'nullable|exists:images,id', 'image' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192', ]); diff --git a/app/Models/Announcement.php b/app/Models/Announcement.php index 0816895..bf2c531 100644 --- a/app/Models/Announcement.php +++ b/app/Models/Announcement.php @@ -8,7 +8,7 @@ class Announcement extends Model { protected $fillable = [ - 'image_id', 'title', 'description', 'link', 'start_date', 'end_date', + 'image_id', 'title', 'description', 'link', 'button_text', 'start_date', 'end_date', ]; protected $casts = [ diff --git a/app/Models/AppVersion.php b/app/Models/AppVersion.php index 60c3d3d..aa04b8d 100644 --- a/app/Models/AppVersion.php +++ b/app/Models/AppVersion.php @@ -8,7 +8,7 @@ class AppVersion extends Model { protected $fillable = [ - 'image_id', 'title', 'description', 'version_name', 'version_code', 'link', + 'image_id', 'title', 'description', 'version_name', 'version_code', 'link', 'button_text', ]; protected $casts = [ diff --git a/database/migrations/2026_06_24_120000_add_button_text_to_announcements_and_app_versions.php b/database/migrations/2026_06_24_120000_add_button_text_to_announcements_and_app_versions.php new file mode 100644 index 0000000..db348ba --- /dev/null +++ b/database/migrations/2026_06_24_120000_add_button_text_to_announcements_and_app_versions.php @@ -0,0 +1,30 @@ +string('button_text')->nullable()->after('link'); + }); + + Schema::table('app_versions', function (Blueprint $table) { + $table->string('button_text')->nullable()->after('link'); + }); + } + + public function down(): void + { + Schema::table('announcements', function (Blueprint $table) { + $table->dropColumn('button_text'); + }); + + Schema::table('app_versions', function (Blueprint $table) { + $table->dropColumn('button_text'); + }); + } +}; From 4a0a19fdff80716eb1ac138dcb058dc762b9ef75 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Sat, 27 Jun 2026 09:58:57 +0330 Subject: [PATCH 2/3] fix --- .../Controllers/SurveyQuestionController.php | 93 ++++++++++++++----- 1 file changed, 69 insertions(+), 24 deletions(-) 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 From 7b10a5f6f39b0c51bd5c1d8a2affd4977815caed Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Sat, 27 Jun 2026 10:13:59 +0330 Subject: [PATCH 3/3] fix --- app/Http/Controllers/CategoryController.php | 10 +++++++-- app/Models/Category.php | 6 ++++- ...7_100000_add_order_to_categories_table.php | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 database/migrations/2026_06_27_100000_add_order_to_categories_table.php diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index 14eefeb..a1ca6b7 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -33,14 +33,14 @@ public function index(Request $request) $query->with('subcategories'); } - $categories = $query->orderBy('name')->get(); + $categories = $query->orderBy('order')->orderBy('name')->get(); // No type filter → also fold in the playlist (music) categories so the // response contains every category across both tables. if (!$request->filled('type')) { $categories = $categories ->concat($this->playlistCategories($request)) - ->sortBy('name') + ->sortBy([['order', 'asc'], ['name', 'asc']]) ->values(); } @@ -88,6 +88,7 @@ public function store(Request $request) Rule::unique('categories', 'name')->where('type', $request->input('type', Category::TYPE_MEDIA)), ], 'type' => ['nullable', Rule::in(Category::TYPES)], + 'order' => 'nullable|integer', 'description' => 'nullable|string', 'icon' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192', ]); @@ -95,6 +96,7 @@ public function store(Request $request) $category = Category::create([ 'name' => $data['name'], 'type' => $data['type'] ?? Category::TYPE_MEDIA, + 'order' => $data['order'] ?? 0, 'description' => $data['description'] ?? null, 'icon' => $request->hasFile('icon') ? $this->storeUpload($request->file('icon'), 'categories/icons') @@ -158,6 +160,7 @@ public function update(Request $request, $id) ->ignore($category->id), ], 'type' => ['nullable', Rule::in(Category::TYPES)], + 'order' => 'nullable|integer', 'description' => 'nullable|string', 'icon' => 'nullable|file|extensions:jpg,jpeg,png,gif,webp,bmp,svg|max:8192', ]); @@ -165,6 +168,9 @@ public function update(Request $request, $id) if (array_key_exists('type', $data) && $data['type'] !== null) { $category->type = $data['type']; } + if (array_key_exists('order', $data) && $data['order'] !== null) { + $category->order = $data['order']; + } if (array_key_exists('name', $data)) { $category->name = $data['name']; } diff --git a/app/Models/Category.php b/app/Models/Category.php index ed3b547..525ea12 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -16,7 +16,11 @@ class Category extends Model self::TYPE_BREATHING_TEMPLATE, ]; - protected $fillable = ['name', 'type', 'description', 'icon']; + protected $fillable = ['name', 'type', 'order', 'description', 'icon']; + + protected $casts = [ + 'order' => 'integer', + ]; protected $appends = ['icon_url']; diff --git a/database/migrations/2026_06_27_100000_add_order_to_categories_table.php b/database/migrations/2026_06_27_100000_add_order_to_categories_table.php new file mode 100644 index 0000000..8b79a0a --- /dev/null +++ b/database/migrations/2026_06_27_100000_add_order_to_categories_table.php @@ -0,0 +1,22 @@ +integer('order')->default(0)->after('type')->index(); + }); + } + + public function down(): void + { + Schema::table('categories', function (Blueprint $table) { + $table->dropColumn('order'); + }); + } +};