Feat leaderboard (#74)

This commit is contained in:
Lin, Hung-Shin authored and GitHub committed 2023-11-23 23:34:18 +08:00
commit 143b84afb4
7 files changed
+154 -4

No files matched your search

+8 -1
View File
@@ -69,10 +69,17 @@ onMounted(async () => {
class="text-text-tertiary h-full w-full"
/>
</button> -->
<p
<button
id="HeaderText"
class="text-text-primary inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
Linux Odyssey
</button>
<p
id="HeaderText"
class="text-text inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
{{ title }}
</p>
+6
View File
@@ -4,6 +4,7 @@ import LoginPage from './views/LoginPage.vue'
import RegisterPage from './views/RegisterPage.vue'
import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
import QuestMapPage from './views/QuestMapPage.vue'
import LeaderboardPage from './views/LeaderboardPage.vue'
import { isLoggedIn } from './utils/auth'
const routes = [
@@ -45,6 +46,11 @@ const routes = [
component: QuestMapPage,
meta: { requiresAuth: true },
},
{
path: '/leaderboard',
name: 'leaderboard',
component: LeaderboardPage,
},
]
const router = createRouter({
+81
View File
@@ -0,0 +1,81 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import DynamicBackground from '../components/DynamicBackground.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue'
import api from '../utils/api'
const leaderboard = ref([])
const toast = useToast()
async function getLeaderboard() {
try {
const res = await api.get('/leaderboard')
leaderboard.value = res.data
} catch (err) {
console.error(err)
toast.error('讀取排行榜失敗')
}
}
onMounted(async () => {
await getLeaderboard()
})
</script>
<template>
<div class="w-screen h-screen bg-bg">
<DynamicBackground class="w-full h-full" />
<div
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="h-[6vh] w-full">
<GameHeaderPart title="排行榜" />
</div>
<div class="w-fit h-[94vh] m-3">
<div
class="h-full bg-bg border-8 border-bg-primary flex flex-col items-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl mb-5">排行榜</h1>
<div class="max-h-300 overflow-auto">
<table class="w-full text-sm text-left text-text">
<thead
class="text-xs text-text-secondary uppercase bg-bg-secondary"
>
<tr>
<th scope="col" class="px-6 py-3">使用者名稱</th>
<th scope="col" class="px-6 py-3">分數</th>
<th scope="col" class="px-6 py-3">完成關卡</th>
</tr>
</thead>
<tbody>
<tr
v-for="user in leaderboard"
:key="user.username"
class="border-b bg-bg"
>
<th
scope="row"
class="px-6 py-4 font-medium text-text-primary whitespace-nowrap"
>
{{ user.username }}
</th>
<td class="px-6 py-4">{{ user.score }}</td>
<td class="px-6 py-4 text-text-secondary">
<span
class="mr-2"
v-for="quest in user.completedQuests"
:key="quest"
>
{{ quest }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,12 @@
import { asyncHandler } from '../../middleware/error.js'
import { leaderboard } from '../../models/userManager.js'
export default asyncHandler(async (req, res) => {
const users = await leaderboard()
const response = users.map((user) => ({
username: user.user.username,
completedQuests: user.completedQuests,
score: user.score * 1000,
}))
res.json(response)
})
+3
View File
@@ -7,6 +7,7 @@ import authRouter from './authRouter.js'
import authRequired from '../../middleware/authRequired.js'
import userRouter from './userRouter.js'
import config from '../../config.js'
import leaderboardController from '../controllers/leaderboardController.js'
const router = Router()
@@ -48,4 +49,6 @@ router.get('/bug-report', (req, res) => {
res.redirect(config.bugReportUrl)
})
router.get('/leaderboard', leaderboardController)
export default router
+43
View File
@@ -22,3 +22,46 @@ export async function createUser(username, email, { password, socialLogin }) {
return user
}
export function leaderboard() {
return UserProfile.aggregate([
{
$project: {
user: true, // Include the user field in the projection
completedQuests: {
$map: {
input: {
$filter: {
input: { $objectToArray: '$progress' }, // Convert the progress map to an array
as: 'prog',
cond: { $eq: ['$$prog.v.completed', true] }, // Filter for completed quests
},
},
as: 'completedProg',
in: '$$completedProg.k', // Extract the quest IDs
},
},
},
},
{
$addFields: {
score: { $size: '$completedQuests' }, // Calculate the size of the completedQuests array
},
},
{
$match: {
score: { $gt: 0 }, // Filter out users who haven't completed any quests
},
},
{ $sort: { score: -1 } },
{
$lookup: {
from: 'users',
localField: 'user',
foreignField: '_id',
as: 'user',
},
},
{ $unwind: '$user' },
])
}
+1 -3
View File
@@ -78,9 +78,7 @@ describe('example helloworld app', () => {
cy.PrepareForGame()
})
it('Check Header', () => {
cy.get('#HeaderText')
.should('contain', 'Hello, Linux World!')
.and('be.visible')
cy.get('#HeaderText').should('contain', 'Linux Odyssey').and('be.visible')
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
})