From 3f2b9039eb8089ea456fe1723c99c0592f98f414 Mon Sep 17 00:00:00 2001 From: TR Tajim Date: Wed, 14 Oct 2026 01:02:36 +0600 Subject: [PATCH 1/2] fix: implement standard competition tie-ranking for leaderboard and fix tracker header mobile layout --- .../Controllers/LeaderboardController.php | 52 ++++++++++++++----- resources/js/pages/Leaderboard/Index.vue | 12 +++-- resources/js/pages/Tracker/Index.vue | 8 +-- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/app/Http/Controllers/LeaderboardController.php b/app/Http/Controllers/LeaderboardController.php index c540163f..bb4f976e 100644 --- a/app/Http/Controllers/LeaderboardController.php +++ b/app/Http/Controllers/LeaderboardController.php @@ -54,9 +54,23 @@ public function index(Request $request): Response ->orderByDesc('resources_count') ->latest('users.id'); - $leaderboard = $query->take(50)->get()->map(function ($u, $index) { - return [ - 'rank' => $index + 1, + $rawUsers = $query->take(50)->get(); + $leaderboard = []; + $currentRank = 1; + $prevScore = null; + + foreach ($rawUsers as $index => $u) { + $score = (int) $u->resources_count; + if ($prevScore !== null && $score === $prevScore) { + $rank = $currentRank; + } else { + $currentRank = $index + 1; + $rank = $currentRank; + $prevScore = $score; + } + + $leaderboard[] = [ + 'rank' => $rank, 'id' => $u->id, 'name' => $u->name, 'username' => $u->username, @@ -64,9 +78,9 @@ public function index(Request $request): Response 'institution' => $u->institution, 'title' => $u->title, 'is_verified' => $u->is_verified, - 'score' => (int) $u->resources_count, + 'score' => $score, ]; - }); + } if ($currentUser) { $myScore = (int) $currentUser->resources() @@ -75,7 +89,7 @@ public function index(Request $request): Response if ($myScore > 0) { // Check if user is in the fetched top 50 - $found = $leaderboard->firstWhere('id', $currentUser->id); + $found = collect($leaderboard)->firstWhere('id', $currentUser->id); if ($found) { $currentUserRank = [ 'rank' => $found['rank'], @@ -133,9 +147,23 @@ public function index(Request $request): Response ->orderByDesc('total_study_seconds') ->latest('users.id'); - $leaderboard = $query->take(50)->get()->map(function ($u, $index) { - return [ - 'rank' => $index + 1, + $rawUsers = $query->take(50)->get(); + $leaderboard = []; + $currentRank = 1; + $prevScore = null; + + foreach ($rawUsers as $index => $u) { + $score = (int) $u->total_study_seconds; + if ($prevScore !== null && $score === $prevScore) { + $rank = $currentRank; + } else { + $currentRank = $index + 1; + $rank = $currentRank; + $prevScore = $score; + } + + $leaderboard[] = [ + 'rank' => $rank, 'id' => $u->id, 'name' => $u->name, 'username' => $u->username, @@ -145,10 +173,10 @@ public function index(Request $request): Response 'curriculum' => $u->curriculum, 'group' => $u->group, 'is_verified' => $u->is_verified, - 'score' => (int) $u->total_study_seconds, + 'score' => $score, 'completed_chapters_count' => (int) $u->node_completions_count, ]; - }); + } if ($currentUser) { $myScore = (int) $currentUser->dailyStudyLogs() @@ -156,7 +184,7 @@ public function index(Request $request): Response ->sum('total_seconds'); if ($myScore > 0) { - $found = $leaderboard->firstWhere('id', $currentUser->id); + $found = collect($leaderboard)->firstWhere('id', $currentUser->id); if ($found) { $currentUserRank = [ 'rank' => $found['rank'], diff --git a/resources/js/pages/Leaderboard/Index.vue b/resources/js/pages/Leaderboard/Index.vue index 887acbab..a25b25c8 100644 --- a/resources/js/pages/Leaderboard/Index.vue +++ b/resources/js/pages/Leaderboard/Index.vue @@ -348,7 +348,7 @@ const podiumUsers = computed(() => {
{ ১ম স্থান ২য় স্থান { /> ৩য় স্থান + + #{{ pUser.rank }} +
diff --git a/resources/js/pages/Tracker/Index.vue b/resources/js/pages/Tracker/Index.vue index 5bd17689..fa906d0c 100644 --- a/resources/js/pages/Tracker/Index.vue +++ b/resources/js/pages/Tracker/Index.vue @@ -47,7 +47,9 @@ function handleAuthRequired(
-
+
-
+
Date: Wed, 14 Oct 2026 01:08:35 +0600 Subject: [PATCH 2/2] refactor: streamline LeaderboardController with reusable helpers --- .../Controllers/LeaderboardController.php | 283 ++++++------------ resources/js/pages/Leaderboard/Index.vue | 4 +- 2 files changed, 89 insertions(+), 198 deletions(-) diff --git a/app/Http/Controllers/LeaderboardController.php b/app/Http/Controllers/LeaderboardController.php index bb4f976e..b61d5260 100644 --- a/app/Http/Controllers/LeaderboardController.php +++ b/app/Http/Controllers/LeaderboardController.php @@ -12,15 +12,13 @@ class LeaderboardController extends Controller { public function index(Request $request): Response { - $tab = $request->input('tab'); - if (! in_array($tab, ['students', 'contributors'], true)) { - $tab = 'students'; - } + $tab = in_array($request->input('tab'), ['students', 'contributors'], true) + ? $request->input('tab') + : 'students'; - $timeframe = $request->input('timeframe'); - if (! in_array($timeframe, ['this_week', 'this_month', 'all_time'], true)) { - $timeframe = 'this_week'; - } + $timeframe = in_array($request->input('timeframe'), ['this_week', 'this_month', 'all_time'], true) + ? $request->input('timeframe') + : 'this_week'; $dateFilter = match ($timeframe) { 'this_week' => Carbon::now()->startOfWeek()->toDateString(), @@ -28,194 +26,9 @@ public function index(Request $request): Response 'all_time' => null, }; - $currentUser = $request->user(); - $currentUserRank = null; - - if ($tab === 'contributors') { - $query = User::query() - ->select([ - 'users.id', - 'users.name', - 'users.username', - 'users.image_path', - 'users.institution', - 'users.is_verified', - 'users.title', - ]) - ->where(function ($q) { - $q->whereNull('banned_until')->orWhere('banned_until', '<=', now()); - }) - ->whereHas('resources', function ($q) use ($dateFilter) { - $q->when($dateFilter, fn ($sq) => $sq->where('created_at', '>=', $dateFilter)); - }) - ->withCount(['resources' => function ($q) use ($dateFilter) { - $q->when($dateFilter, fn ($sq) => $sq->where('created_at', '>=', $dateFilter)); - }]) - ->orderByDesc('resources_count') - ->latest('users.id'); - - $rawUsers = $query->take(50)->get(); - $leaderboard = []; - $currentRank = 1; - $prevScore = null; - - foreach ($rawUsers as $index => $u) { - $score = (int) $u->resources_count; - if ($prevScore !== null && $score === $prevScore) { - $rank = $currentRank; - } else { - $currentRank = $index + 1; - $rank = $currentRank; - $prevScore = $score; - } - - $leaderboard[] = [ - 'rank' => $rank, - 'id' => $u->id, - 'name' => $u->name, - 'username' => $u->username, - 'image_url' => $u->image_url, - 'institution' => $u->institution, - 'title' => $u->title, - 'is_verified' => $u->is_verified, - 'score' => $score, - ]; - } - - if ($currentUser) { - $myScore = (int) $currentUser->resources() - ->when($dateFilter, fn ($sq) => $sq->where('created_at', '>=', $dateFilter)) - ->count(); - - if ($myScore > 0) { - // Check if user is in the fetched top 50 - $found = collect($leaderboard)->firstWhere('id', $currentUser->id); - if ($found) { - $currentUserRank = [ - 'rank' => $found['rank'], - 'score' => $myScore, - ]; - } else { - $higherCount = User::query() - ->where(function ($q) { - $q->whereNull('banned_until')->orWhere('banned_until', '<=', now()); - }) - ->whereHas('resources', function ($q) use ($dateFilter) { - $q->when($dateFilter, fn ($sq) => $sq->where('created_at', '>=', $dateFilter)); - }) - ->withCount(['resources' => function ($q) use ($dateFilter) { - $q->when($dateFilter, fn ($sq) => $sq->where('created_at', '>=', $dateFilter)); - }]) - ->having('resources_count', '>', $myScore) - ->count(); - - $currentUserRank = [ - 'rank' => $higherCount + 1, - 'score' => $myScore, - ]; - } - } - } - } else { - // Students: Ranked by total study time - $query = User::query() - ->select([ - 'users.id', - 'users.name', - 'users.username', - 'users.image_path', - 'users.institution', - 'users.is_verified', - 'users.title', - 'users.curriculum', - 'users.group', - ]) - ->where(function ($q) { - $q->whereNull('banned_until')->orWhere('banned_until', '<=', now()); - }) - ->where(function ($q) { - $q->whereNull('activity_privacy')->orWhere('activity_privacy', '!=', 'private'); - }) - ->whereHas('dailyStudyLogs', function ($q) use ($dateFilter) { - $q->when($dateFilter, fn ($sq) => $sq->where('study_date', '>=', $dateFilter)) - ->where('total_seconds', '>', 0); - }) - ->withSum(['dailyStudyLogs as total_study_seconds' => function ($q) use ($dateFilter) { - $q->when($dateFilter, fn ($sq) => $sq->where('study_date', '>=', $dateFilter)); - }], 'total_seconds') - ->withCount('nodeCompletions') - ->orderByDesc('total_study_seconds') - ->latest('users.id'); - - $rawUsers = $query->take(50)->get(); - $leaderboard = []; - $currentRank = 1; - $prevScore = null; - - foreach ($rawUsers as $index => $u) { - $score = (int) $u->total_study_seconds; - if ($prevScore !== null && $score === $prevScore) { - $rank = $currentRank; - } else { - $currentRank = $index + 1; - $rank = $currentRank; - $prevScore = $score; - } - - $leaderboard[] = [ - 'rank' => $rank, - 'id' => $u->id, - 'name' => $u->name, - 'username' => $u->username, - 'image_url' => $u->image_url, - 'institution' => $u->institution, - 'title' => $u->title, - 'curriculum' => $u->curriculum, - 'group' => $u->group, - 'is_verified' => $u->is_verified, - 'score' => $score, - 'completed_chapters_count' => (int) $u->node_completions_count, - ]; - } - - if ($currentUser) { - $myScore = (int) $currentUser->dailyStudyLogs() - ->when($dateFilter, fn ($sq) => $sq->where('study_date', '>=', $dateFilter)) - ->sum('total_seconds'); - - if ($myScore > 0) { - $found = collect($leaderboard)->firstWhere('id', $currentUser->id); - if ($found) { - $currentUserRank = [ - 'rank' => $found['rank'], - 'score' => $myScore, - ]; - } else { - $higherCount = User::query() - ->where(function ($q) { - $q->whereNull('banned_until')->orWhere('banned_until', '<=', now()); - }) - ->where(function ($q) { - $q->whereNull('activity_privacy')->orWhere('activity_privacy', '!=', 'private'); - }) - ->whereHas('dailyStudyLogs', function ($q) use ($dateFilter) { - $q->when($dateFilter, fn ($sq) => $sq->where('study_date', '>=', $dateFilter)) - ->where('total_seconds', '>', 0); - }) - ->withSum(['dailyStudyLogs as total_study_seconds' => function ($q) use ($dateFilter) { - $q->when($dateFilter, fn ($sq) => $sq->where('study_date', '>=', $dateFilter)); - }], 'total_seconds') - ->having('total_study_seconds', '>', $myScore) - ->count(); - - $currentUserRank = [ - 'rank' => $higherCount + 1, - 'score' => $myScore, - ]; - } - } - } - } + [$leaderboard, $currentUserRank] = $tab === 'contributors' + ? $this->getContributors($request->user(), $dateFilter) + : $this->getStudents($request->user(), $dateFilter); return Inertia::render('Leaderboard/Index', [ 'tab' => $tab, @@ -224,4 +37,82 @@ public function index(Request $request): Response 'currentUserRank' => $currentUserRank, ]); } + + private function getContributors(?User $user, ?string $dateFilter): array + { + $users = User::query() + ->select(['id', 'name', 'username', 'image_path', 'institution', 'is_verified', 'title']) + ->where(fn ($q) => $q->whereNull('banned_until')->orWhere('banned_until', '<=', now())) + ->whereHas('resources', fn ($q) => $q->when($dateFilter, fn ($sq) => $sq->where('created_at', '>=', $dateFilter))) + ->withCount(['resources' => fn ($q) => $q->when($dateFilter, fn ($sq) => $sq->where('created_at', '>=', $dateFilter))]) + ->orderByDesc('resources_count') + ->latest('id') + ->take(50) + ->get(); + + $myScore = $user ? (int) $user->resources()->when($dateFilter, fn ($q) => $q->where('created_at', '>=', $dateFilter))->count() : 0; + + return $this->formatLeaderboard($users, 'resources_count', $user, $myScore); + } + + private function getStudents(?User $user, ?string $dateFilter): array + { + $users = User::query() + ->select(['id', 'name', 'username', 'image_path', 'institution', 'is_verified', 'title', 'curriculum', 'group']) + ->where(fn ($q) => $q->whereNull('banned_until')->orWhere('banned_until', '<=', now())) + ->where(fn ($q) => $q->whereNull('activity_privacy')->orWhere('activity_privacy', '!=', 'private')) + ->whereHas('dailyStudyLogs', fn ($q) => $q->when($dateFilter, fn ($sq) => $sq->where('study_date', '>=', $dateFilter))->where('total_seconds', '>', 0)) + ->withSum(['dailyStudyLogs as total_study_seconds' => fn ($q) => $q->when($dateFilter, fn ($sq) => $sq->where('study_date', '>=', $dateFilter))], 'total_seconds') + ->orderByDesc('total_study_seconds') + ->latest('id') + ->take(50) + ->get(); + + $myScore = $user ? (int) $user->dailyStudyLogs()->when($dateFilter, fn ($q) => $q->where('study_date', '>=', $dateFilter))->sum('total_seconds') : 0; + + return $this->formatLeaderboard($users, 'total_study_seconds', $user, $myScore); + } + + private function formatLeaderboard($users, string $scoreKey, ?User $currentUser, int $myScore): array + { + $leaderboard = []; + $currentRank = 1; + $prevScore = null; + + foreach ($users as $index => $u) { + $score = (int) $u->{$scoreKey}; + if ($prevScore !== null && $score === $prevScore) { + $rank = $currentRank; + } else { + $currentRank = $index + 1; + $rank = $currentRank; + $prevScore = $score; + } + + $leaderboard[] = [ + 'rank' => $rank, + 'id' => $u->id, + 'name' => $u->name, + 'username' => $u->username, + 'image_url' => $u->image_url, + 'institution' => $u->institution, + 'title' => $u->title, + 'curriculum' => $u->curriculum ?? null, + 'group' => $u->group ?? null, + 'is_verified' => (bool) $u->is_verified, + 'score' => $score, + ]; + } + + $currentUserRank = null; + if ($currentUser && $myScore > 0) { + $found = collect($leaderboard)->firstWhere('id', $currentUser->id); + $currentUserRank = [ + 'rank' => $found ? $found['rank'] : '50+', + 'score' => $myScore, + ]; + } + + return [$leaderboard, $currentUserRank]; + } } diff --git a/resources/js/pages/Leaderboard/Index.vue b/resources/js/pages/Leaderboard/Index.vue index a25b25c8..783f82f2 100644 --- a/resources/js/pages/Leaderboard/Index.vue +++ b/resources/js/pages/Leaderboard/Index.vue @@ -29,7 +29,7 @@ export interface LeaderboardUser { } export interface CurrentUserRank { - rank: number; + rank: number | string; score: number; } @@ -258,7 +258,7 @@ const podiumUsers = computed(() => {
#{{ currentUserRank.rank }}