mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 11:30: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"
|
class="text-text-tertiary h-full w-full"
|
||||||
/>
|
/>
|
||||||
</button> -->
|
</button> -->
|
||||||
<p
|
<button
|
||||||
id="HeaderText"
|
id="HeaderText"
|
||||||
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
||||||
style="font-size: 2vh"
|
style="font-size: 2vh"
|
||||||
|
>
|
||||||
|
Linux Odyssey
|
||||||
|
</button>
|
||||||
|
<p
|
||||||
|
id="HeaderText"
|
||||||
|
class="text-text inline-block font-bold whitespace-nowrap"
|
||||||
|
style="font-size: 2vh"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import LoginPage from './views/LoginPage.vue'
|
|||||||
import RegisterPage from './views/RegisterPage.vue'
|
import RegisterPage from './views/RegisterPage.vue'
|
||||||
import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
|
import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
|
||||||
import QuestMapPage from './views/QuestMapPage.vue'
|
import QuestMapPage from './views/QuestMapPage.vue'
|
||||||
|
import LeaderboardPage from './views/LeaderboardPage.vue'
|
||||||
import { isLoggedIn } from './utils/auth'
|
import { isLoggedIn } from './utils/auth'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -45,6 +46,11 @@ const routes = [
|
|||||||
component: QuestMapPage,
|
component: QuestMapPage,
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/leaderboard',
|
||||||
|
name: 'leaderboard',
|
||||||
|
component: LeaderboardPage,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
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 authRequired from '../../middleware/authRequired.js'
|
||||||
import userRouter from './userRouter.js'
|
import userRouter from './userRouter.js'
|
||||||
import config from '../../config.js'
|
import config from '../../config.js'
|
||||||
|
import leaderboardController from '../controllers/leaderboardController.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
@@ -48,4 +49,6 @@ router.get('/bug-report', (req, res) => {
|
|||||||
res.redirect(config.bugReportUrl)
|
res.redirect(config.bugReportUrl)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.get('/leaderboard', leaderboardController)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
@@ -22,3 +22,46 @@ export async function createUser(username, email, { password, socialLogin }) {
|
|||||||
|
|
||||||
return user
|
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()
|
cy.PrepareForGame()
|
||||||
})
|
})
|
||||||
it('Check Header', () => {
|
it('Check Header', () => {
|
||||||
cy.get('#HeaderText')
|
cy.get('#HeaderText').should('contain', 'Linux Odyssey').and('be.visible')
|
||||||
.should('contain', 'Hello, Linux World!')
|
|
||||||
.and('be.visible')
|
|
||||||
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
|
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
|
||||||
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
|
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in new issue
Block a user