From dc3c21f88532c04c4405b694e1bd8e7b1ff6c488 Mon Sep 17 00:00:00 2001 From: Pascal Syma Date: Sat, 9 Aug 2025 19:14:21 +0200 Subject: [PATCH] Add end showcase --- app/components/showcase/NumberCard.vue | 28 ++ app/lang/de.json | 22 ++ app/lang/en.json | 22 ++ app/layouts/full.vue | 5 + app/pages/hunt/[huntSlug]/showcase.vue | 288 ++++++++++++++++++ .../admin/answer/[answerId]/review.post.ts | 2 +- .../api/admin/hunt/[huntId]/showcase.get.ts | 181 +++++++++++ 7 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 app/components/showcase/NumberCard.vue create mode 100644 app/layouts/full.vue create mode 100644 app/pages/hunt/[huntSlug]/showcase.vue create mode 100644 server/api/admin/hunt/[huntId]/showcase.get.ts diff --git a/app/components/showcase/NumberCard.vue b/app/components/showcase/NumberCard.vue new file mode 100644 index 0000000..88eefe5 --- /dev/null +++ b/app/components/showcase/NumberCard.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/app/lang/de.json b/app/lang/de.json index 7982d00..3be0c85 100644 --- a/app/lang/de.json +++ b/app/lang/de.json @@ -53,6 +53,28 @@ "no_hunt_joined": "Noch keinem Hunt beigetreten!", "open_to_join": "Bereit zum Beitreten:", "start": "Start: {start}" + }, + "showcase": { + "showcase_for": "Abschluss für", + "crew": "Crew", + "quests": "Quests", + "users": "Spieler", + "teams": "Teams", + "total_points": "Punkte erzielt", + "pictures": "Bilder", + "leaderboard": "Rangliste", + "third_place": "Dritter Platz", + "second_place": "Zweiter Platz", + "first_place": "Erster Platz", + "points": "Punkte", + "answers": "Antworten", + "table": { + "answers": "Antworten", + "place": "Platz", + "name": "Name", + "description": "Beschreibung", + "score": "Punkte" + } } }, "validation": { diff --git a/app/lang/en.json b/app/lang/en.json index 1e3aaea..4a1acac 100644 --- a/app/lang/en.json +++ b/app/lang/en.json @@ -53,6 +53,28 @@ "no_hunt_joined": "No hunt joined yet!", "open_to_join": "Open to join hunts:", "start": "Start: {start}" + }, + "showcase": { + "showcase_for": "Showcase for", + "crew": "Crew", + "quests": "Quests", + "users": "Users", + "teams": "Teams", + "total_points": "Total points earned", + "pictures": "Pictures", + "leaderboard": "Leaderboard", + "third_place": "Third place", + "second_place": "Second place", + "first_place": "First place", + "points": "Points", + "answers": "Answers", + "table": { + "answers": "Answers", + "place": "Place", + "name": "Name", + "description": "Description", + "score": "Score" + } } }, "validation": { diff --git a/app/layouts/full.vue b/app/layouts/full.vue new file mode 100644 index 0000000..53998b1 --- /dev/null +++ b/app/layouts/full.vue @@ -0,0 +1,5 @@ + diff --git a/app/pages/hunt/[huntSlug]/showcase.vue b/app/pages/hunt/[huntSlug]/showcase.vue new file mode 100644 index 0000000..a65d97a --- /dev/null +++ b/app/pages/hunt/[huntSlug]/showcase.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/server/api/admin/answer/[answerId]/review.post.ts b/server/api/admin/answer/[answerId]/review.post.ts index 3638e0f..1c2dd0a 100644 --- a/server/api/admin/answer/[answerId]/review.post.ts +++ b/server/api/admin/answer/[answerId]/review.post.ts @@ -48,7 +48,7 @@ export default defineEventHandler(async (event) => { const { score, review } = await useValidatedBody( event, z.object({ - score: z.number().min(0).nullable(), + score: z.int().min(0).max(32767).nullable(), review: z.string().max(256).nullable() }) ); diff --git a/server/api/admin/hunt/[huntId]/showcase.get.ts b/server/api/admin/hunt/[huntId]/showcase.get.ts new file mode 100644 index 0000000..b3a64f0 --- /dev/null +++ b/server/api/admin/hunt/[huntId]/showcase.get.ts @@ -0,0 +1,181 @@ +import { useValidatedParams, z, zh } from 'h3-zod'; +import prisma from '~~/lib/prisma'; + +export default defineEventHandler(async (event) => { + // TODO: re-enable perms + // const user = await requireUserSession(event); + // + // if (user.user.role !== 'ADMIN') { + // throw createError({ + // status: 403, + // statusText: 'Not allowed!' + // }); + // } + // const userId = user.user.id; + const { huntId } = await useValidatedParams( + event, + z.object({ + huntId: zh.numAsString + }) + ); + + const _hunt = await prisma.hunt.findUnique({ + where: { + id: huntId + // OR: [ + // { + // creatorId: userId + // }, + // { + // members: { + // some: { + // memberId: userId + // } + // } + // } + // ] + }, + select: { + id: true, + name: true, + start: true, + end: true, + + _count: { + select: { + teams: true, + members: true, + quests: true + } + }, + + teams: { + select: { + _count: { + select: { + members: true, + answers: { + where: { + score: { gt: 0 } + } + } + } + }, + id: true, + name: true, + description: true, + answers: { + select: { + score: true + }, + where: { + score: { not: null } + } + } + } + }, + + quests: { + select: { + id: true, + title: true, + question: true, + textSolution: true, + points: true, + extraPoints: true, + + answers: { + select: { + id: true, + text: true, + score: true, + + picture: { + select: { + url: true, + createdAt: true, + creator: { + select: { + id: true, + name: true + } + } + } + }, + team: { + select: { + id: true, + name: true + } + } + }, + where: { + showcase: true + }, + orderBy: [ + { rankingValue: { sort: 'asc', nulls: 'last' } }, + { score: { sort: 'asc', nulls: 'last' } }, + { createdAt: 'asc' } + ] + } + }, + where: { + OR: [ + { + answers: { + some: { + showcase: true + } + } + } + // TODO: showcase solutions to quests + // { + // textSolution: { not: null } + // } + ] + } + } + } + }); + + if (!_hunt) { + throw createError({ status: 404, statusText: 'Not found!' }); + } + + const { teams, ...hunt } = _hunt; + + const userCount = teams.reduce( + (sum, team) => sum + team._count.members + 1, + 0 + ); + + const leaderboard = teams + .map(({ name, id, description, answers, _count }) => ({ + name, + id, + description, + answers: _count.answers, + score: answers.reduce((sum, a) => sum + (a.score || 0), 0) + })) + .toSorted((a, b) => b.score - a.score); + + const totalScore = leaderboard.reduce((sum, team) => sum + team.score, 0); + + const pictureCount = await prisma.file.count({ + where: { + answer: { + quest: { + huntId + } + } + } + }); + + return { + userCount, + pictureCount, + totalScore, + leaderboard, + ...hunt + }; +});