Skip to content
This repository was archived by the owner on Oct 15, 2025. It is now read-only.

Commit 1d1f45b

Browse files
committed
feat(overview): Implement Leaderboard
1 parent 19dfaf9 commit 1d1f45b

File tree

4 files changed

+83
-1
lines changed

4 files changed

+83
-1
lines changed

src/Controller/OverviewCardsController.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,18 @@ public function eventDailyChart(
183183
'chart' => $chart,
184184
]);
185185
}
186+
187+
/**
188+
* List the top users with the highest solved questions.
189+
*/
190+
#[Route('/leaderboard', name: 'leaderboard')]
191+
public function leaderboard(
192+
SolutionEventRepository $solutionEventRepository,
193+
): Response {
194+
$leaderboard = $solutionEventRepository->listLeaderboard('7 days');
195+
196+
return $this->render('overview/cards/leaderboard.html.twig', [
197+
'leaderboard' => $leaderboard,
198+
]);
199+
}
186200
}

src/Repository/SolutionEventRepository.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,50 @@ public function getSolveState(Question $question, User $user): ?SolutionEventSta
133133

134134
return null;
135135
}
136+
137+
/**
138+
* List the users leaderboard by the number of questions they have solved.
139+
*
140+
* @param string $interval The interval to count the leaderboard
141+
*
142+
* @return list<array{user: User, count: int}> The leaderboard
143+
*/
144+
public function listLeaderboard(string $interval): array
145+
{
146+
$startedFrom = new \DateTimeImmutable("-$interval");
147+
148+
$qb = $this->createQueryBuilder('e')
149+
->from(User::class, 'u')
150+
->select('u AS user', 'COUNT(e.id) AS count')
151+
->where('e.submitter = u')
152+
->andWhere('e.status = :status')
153+
->andWhere('e.createdAt >= :startedFrom')
154+
->groupBy('u.id')
155+
->orderBy('count', 'DESC')
156+
->setParameter('status', SolutionEventStatus::Passed)
157+
->setParameter('startedFrom', $startedFrom);
158+
159+
$result = $qb->getQuery()->getResult();
160+
\assert(\is_array($result) && array_is_list($result));
161+
162+
/**
163+
* @var list<array{user: User, count: int}> $leaderboard
164+
*/
165+
$leaderboard = [];
166+
167+
foreach ($result as $item) {
168+
\assert(\is_array($item));
169+
\assert(\array_key_exists('user', $item));
170+
\assert(\array_key_exists('count', $item));
171+
\assert($item['user'] instanceof User);
172+
\assert(\is_int($item['count']));
173+
174+
$leaderboard[] = [
175+
'user' => $item['user'],
176+
'count' => $item['count'],
177+
];
178+
}
179+
180+
return $leaderboard;
181+
}
136182
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{% extends 'base.html.twig' %}
2+
3+
{% block body %}
4+
<turbo-frame id="leaderboard">
5+
<table class="table table-hover table-light">
6+
<thead>
7+
<tr>
8+
<th scope="col">名字</th>
9+
<th scope="col">解題數</th>
10+
</tr>
11+
</thead>
12+
<tbody>
13+
{% for item in leaderboard %}
14+
<tr>
15+
<th scope="row">{{ item.user.name }}</th>
16+
<td>{{ item.count }}</td>
17+
</tr>
18+
{% endfor %}
19+
</tbody>
20+
</table>
21+
</turbo-frame>
22+
{% endblock %}

templates/overview/index.html.twig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
<section class="app-overview-dashboard__leaderboard">
4848
<h3 class="mb-3">週排行榜</h3>
49-
49+
<turbo-frame id="leaderboard" src="{{ path('app_overview_cards_leaderboard') }}"></turbo-frame>
5050
</section>
5151
</main>
5252
{% endblock %}

0 commit comments

Comments
 (0)