mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Feat leaderboard (#74)
This commit is contained in:
7 files changed
+154
-4
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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
|
||||
@@ -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' },
|
||||
])
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user