From 854297efaa71858d434dcc6edfcd87655d297e88 Mon Sep 17 00:00:00 2001 From: uwumarogie Date: Mon, 24 Nov 2025 12:18:58 +0100 Subject: [PATCH 1/5] chore: large refactor of the codebase --- src/app/(safespace)/achievements/page.tsx | 2 +- src/app/(safespace)/layout.tsx | 6 +- .../daten-verarbeitung/kapitel1/quiz/page.tsx | 4 +- .../kapitel2/analyse/level/0/part/1/page.tsx | 2 +- .../kapitel2/analyse/level/1/part/1/page.tsx | 2 +- .../kapitel2/components/profile-analysis.tsx | 2 +- .../space/daten-verarbeitung/recap/page.tsx | 2 +- src/app/(safespace)/space/masterquiz/page.tsx | 4 +- .../space/passwort/builder/page.tsx | 2 +- .../(safespace)/space/passwort/quiz/page.tsx | 2 +- .../space/passwort/strength/page.tsx | 4 +- .../space/phishing/assign/page.tsx | 2 +- .../space/phishing/profiling/page.tsx | 2 +- .../space/privatsphaere/perso/perso.tsx | 2 +- .../space/privatsphaere/quiz/page.tsx | 4 +- .../(safespace)/space/rechte/dsgvo/page.tsx | 2 +- .../(safespace)/space/rechte/input/action.ts | 13 +- .../(safespace)/space/rechte/input/page.tsx | 2 +- .../(safespace)/space/rechte/quiz/page.tsx | 6 +- src/app/api/achievements/route.ts | 112 +++++++++++ src/app/api/{get-answer => answers}/route.ts | 52 +++-- src/app/api/create-user/route.ts | 69 ------- src/app/api/get-achievement/route.ts | 40 ---- src/app/api/get-highScores/route.ts | 47 ----- src/app/api/get-user/route.ts | 44 ----- src/app/api/highscores/route.ts | 104 ++++++++++ src/app/api/leaderboard/route.ts | 66 ++++--- src/app/api/set-achievement/route.ts | 69 ------- src/app/api/set-highScore/route.ts | 38 ---- src/app/api/users/route.ts | 136 +++++++++++++ src/app/layout.tsx | 4 +- src/app/page.tsx | 4 +- src/components/Quiz/helper.ts | 6 +- src/components/Quiz/quiz-list.tsx | 10 +- src/components/Quiz/quiz.tsx | 39 ++-- src/components/button.tsx | 16 +- src/components/exercise-navigation.tsx | 2 +- src/components/hint-card.tsx | 4 +- src/components/instagram-profile.tsx | 4 +- .../multi-player.tsx | 0 .../single-player.tsx | 0 .../desktop-navigation}/desktop-nav.tsx | 2 +- .../desktop-section-item.tsx | 2 +- .../desktop-navigation}/desktop-section.tsx | 12 +- .../mobile-navigation}/mobile-nav.tsx | 2 +- .../mobile-navigation}/mobile-overlay.tsx | 0 .../mobile-section-item.tsx | 2 +- .../mobile-navigation}/mobile-section.tsx | 2 +- .../Section.tsx => nav-bar/section.tsx} | 4 +- src/components/privacy-quiz.tsx | 6 +- src/components/progress-bar.tsx | 4 +- src/components/robot-in-passwort.tsx | 4 +- src/components/robot-introduction.tsx | 2 +- src/components/robot/robot.tsx | 4 +- src/components/video-player.tsx | 8 +- src/lib/api/error-handler.ts | 35 ++++ src/lib/api/response.ts | 13 ++ src/lib/constants/endpoints.ts | 9 + src/lib/constants/http.ts | 11 ++ src/lib/utils/fetch.ts | 33 ++++ src/lib/utils/storage.ts | 31 +++ src/server/database/connection.ts | 2 +- .../message-provider.tsx | 2 +- .../notifications-provider.tsx | 2 +- src/services/user/LocalUserService.ts | 24 +-- src/services/user/PersistUserService.ts | 178 ++++++++---------- src/services/user/UserService.ts | 12 +- src/util/passwort/password-quiz-data.tsx | 2 +- 68 files changed, 759 insertions(+), 580 deletions(-) create mode 100644 src/app/api/achievements/route.ts rename src/app/api/{get-answer => answers}/route.ts (69%) delete mode 100644 src/app/api/create-user/route.ts delete mode 100644 src/app/api/get-achievement/route.ts delete mode 100644 src/app/api/get-highScores/route.ts delete mode 100644 src/app/api/get-user/route.ts create mode 100644 src/app/api/highscores/route.ts delete mode 100644 src/app/api/set-achievement/route.ts delete mode 100644 src/app/api/set-highScore/route.ts create mode 100644 src/app/api/users/route.ts rename src/components/{LandingPage => landing-page}/multi-player.tsx (100%) rename src/components/{LandingPage => landing-page}/single-player.tsx (100%) rename src/components/{NavBar/DesktopNavigation => nav-bar/desktop-navigation}/desktop-nav.tsx (99%) rename src/components/{NavBar/DesktopNavigation => nav-bar/desktop-navigation}/desktop-section-item.tsx (96%) rename src/components/{NavBar/DesktopNavigation => nav-bar/desktop-navigation}/desktop-section.tsx (86%) rename src/components/{NavBar/MobileNavigation => nav-bar/mobile-navigation}/mobile-nav.tsx (96%) rename src/components/{NavBar/MobileNavigation => nav-bar/mobile-navigation}/mobile-overlay.tsx (100%) rename src/components/{NavBar/MobileNavigation => nav-bar/mobile-navigation}/mobile-section-item.tsx (94%) rename src/components/{NavBar/MobileNavigation => nav-bar/mobile-navigation}/mobile-section.tsx (95%) rename src/components/{NavBar/Section.tsx => nav-bar/section.tsx} (93%) create mode 100644 src/lib/api/error-handler.ts create mode 100644 src/lib/api/response.ts create mode 100644 src/lib/constants/endpoints.ts create mode 100644 src/lib/constants/http.ts create mode 100644 src/lib/utils/fetch.ts create mode 100644 src/lib/utils/storage.ts rename src/services/{notfication => notification}/message-provider.tsx (97%) rename src/services/{notfication => notification}/notifications-provider.tsx (97%) diff --git a/src/app/(safespace)/achievements/page.tsx b/src/app/(safespace)/achievements/page.tsx index 2b54897b..94750fe2 100644 --- a/src/app/(safespace)/achievements/page.tsx +++ b/src/app/(safespace)/achievements/page.tsx @@ -1,6 +1,6 @@ "use client"; -import AchievementCard from "@/components/Achievements/achievement-card"; +import AchievementCard from "@/components/achievements/achievement-card"; import { useEffect, useState } from "react"; import { ProgressBar } from "@/components/progress-bar"; import { Achievement, AchievementData } from "@/util/achievement-data"; diff --git a/src/app/(safespace)/layout.tsx b/src/app/(safespace)/layout.tsx index 9f2a82b1..3f0b4352 100644 --- a/src/app/(safespace)/layout.tsx +++ b/src/app/(safespace)/layout.tsx @@ -1,11 +1,11 @@ "use client"; -import { DesktopNav } from "@/components/NavBar/DesktopNavigation/desktop-nav"; -import { MobileNav } from "@/components/NavBar/MobileNavigation/mobile-nav"; +import { DesktopNav } from "@/components/nav-bar/desktop-navigation/desktop-nav"; +import { MobileNav } from "@/components/nav-bar/mobile-navigation/mobile-nav"; import { usePathname, useRouter } from "next/navigation"; import React, { useEffect, useState } from "react"; import clsx from "clsx"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { AchievementId } from "@/util/achievement-data"; import { getUserService } from "@/services/user/UserService"; diff --git a/src/app/(safespace)/space/daten-verarbeitung/kapitel1/quiz/page.tsx b/src/app/(safespace)/space/daten-verarbeitung/kapitel1/quiz/page.tsx index 28bd52e7..b43cd88d 100644 --- a/src/app/(safespace)/space/daten-verarbeitung/kapitel1/quiz/page.tsx +++ b/src/app/(safespace)/space/daten-verarbeitung/kapitel1/quiz/page.tsx @@ -1,8 +1,8 @@ "use client"; import { useRouter } from "next/navigation"; -import QuizList from "@/components/Quiz/quiz-list"; -import { QuizParams } from "@/components/Quiz/quiz"; +import QuizList from "@/components/quiz/quiz-list"; +import { QuizParams } from "@/components/quiz/quiz"; import { PersistUserService } from "@/services/user/PersistUserService"; import { AchievementId } from "@/util/achievement-data"; import { getUserService } from "@/services/user/UserService"; diff --git a/src/app/(safespace)/space/daten-verarbeitung/kapitel2/analyse/level/0/part/1/page.tsx b/src/app/(safespace)/space/daten-verarbeitung/kapitel2/analyse/level/0/part/1/page.tsx index aa232c1c..44ce04ad 100644 --- a/src/app/(safespace)/space/daten-verarbeitung/kapitel2/analyse/level/0/part/1/page.tsx +++ b/src/app/(safespace)/space/daten-verarbeitung/kapitel2/analyse/level/0/part/1/page.tsx @@ -16,7 +16,7 @@ import { } from "@phosphor-icons/react"; import clsx from "clsx"; import Button from "@/components/button"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import Link from "next/link"; import TagList from "@/app/(safespace)/space/daten-verarbeitung/kapitel2/analyse/components/tag-list"; diff --git a/src/app/(safespace)/space/daten-verarbeitung/kapitel2/analyse/level/1/part/1/page.tsx b/src/app/(safespace)/space/daten-verarbeitung/kapitel2/analyse/level/1/part/1/page.tsx index 34054d36..8e644683 100644 --- a/src/app/(safespace)/space/daten-verarbeitung/kapitel2/analyse/level/1/part/1/page.tsx +++ b/src/app/(safespace)/space/daten-verarbeitung/kapitel2/analyse/level/1/part/1/page.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import Task from "@/components/task"; import Button from "@/components/button"; import clsx from "clsx"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { useRouter } from "next/navigation"; import { AchievementId } from "@/util/achievement-data"; import { CheckCircle, Info } from "@phosphor-icons/react"; diff --git a/src/app/(safespace)/space/daten-verarbeitung/kapitel2/components/profile-analysis.tsx b/src/app/(safespace)/space/daten-verarbeitung/kapitel2/components/profile-analysis.tsx index 5e93ad53..e4d083d7 100644 --- a/src/app/(safespace)/space/daten-verarbeitung/kapitel2/components/profile-analysis.tsx +++ b/src/app/(safespace)/space/daten-verarbeitung/kapitel2/components/profile-analysis.tsx @@ -7,7 +7,7 @@ import Robot from "@/components/robot/robot"; import clsx from "clsx"; import { HintCard } from "@/components/hint-card"; import { distance } from "fastest-levenshtein"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import Link from "next/link"; type ProfileAnalysisProps = { diff --git a/src/app/(safespace)/space/daten-verarbeitung/recap/page.tsx b/src/app/(safespace)/space/daten-verarbeitung/recap/page.tsx index 5c0958c7..821f0fa4 100644 --- a/src/app/(safespace)/space/daten-verarbeitung/recap/page.tsx +++ b/src/app/(safespace)/space/daten-verarbeitung/recap/page.tsx @@ -7,7 +7,7 @@ import clsx from "clsx"; import AnimatedText from "@/components/animated/animated-text"; import { useRouter } from "next/navigation"; import { AchievementId } from "@/util/achievement-data"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { getUserService } from "@/services/user/UserService"; export type State = { diff --git a/src/app/(safespace)/space/masterquiz/page.tsx b/src/app/(safespace)/space/masterquiz/page.tsx index 343f1bb8..c66df636 100644 --- a/src/app/(safespace)/space/masterquiz/page.tsx +++ b/src/app/(safespace)/space/masterquiz/page.tsx @@ -1,8 +1,8 @@ "use client"; import { useRouter } from "next/navigation"; -import { QuizParams } from "@/components/Quiz/quiz"; -import QuizList from "@/components/Quiz/quiz-list"; +import { QuizParams } from "@/components/quiz/quiz"; +import QuizList from "@/components/quiz/quiz-list"; import { AchievementId } from "@/util/achievement-data"; const quizzes: QuizParams[] = [ diff --git a/src/app/(safespace)/space/passwort/builder/page.tsx b/src/app/(safespace)/space/passwort/builder/page.tsx index 17c3c606..addb1ebd 100644 --- a/src/app/(safespace)/space/passwort/builder/page.tsx +++ b/src/app/(safespace)/space/passwort/builder/page.tsx @@ -4,7 +4,7 @@ import { ActionCard } from "@/components/action-card"; import { HintCard } from "@/components/hint-card"; import Button from "@/components/button"; import { InputValidation } from "@/components/input-validation"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { useEffect, useState } from "react"; import { calculateBruteForceTime } from "@/util/passwort/passwort-validation"; import { AchievementId } from "@/util/achievement-data"; diff --git a/src/app/(safespace)/space/passwort/quiz/page.tsx b/src/app/(safespace)/space/passwort/quiz/page.tsx index f001766d..bf3dff3d 100644 --- a/src/app/(safespace)/space/passwort/quiz/page.tsx +++ b/src/app/(safespace)/space/passwort/quiz/page.tsx @@ -8,7 +8,7 @@ import { replaceCharacter, topTenPasswords, } from "@/util/passwort/password-quiz-data"; -import QuizList from "@/components/Quiz/quiz-list"; +import QuizList from "@/components/quiz/quiz-list"; import { AchievementId } from "@/util/achievement-data"; const hintCards = [ diff --git a/src/app/(safespace)/space/passwort/strength/page.tsx b/src/app/(safespace)/space/passwort/strength/page.tsx index 940ef347..4ed21c4a 100644 --- a/src/app/(safespace)/space/passwort/strength/page.tsx +++ b/src/app/(safespace)/space/passwort/strength/page.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import Button from "@/components/button"; import Image from "next/image"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import clsx from "clsx"; import { passwordData, @@ -41,7 +41,7 @@ export default function PasswordStrength() { const context = getUserService(); const fetchHighScore = async () => { const loadedHighScore = await context.getHighScore("PASSWORD_STRENGTH"); - setHighscore(loadedHighScore); + setHighscore(typeof loadedHighScore === "number" ? loadedHighScore : 0); }; const setAchievement = async () => { diff --git a/src/app/(safespace)/space/phishing/assign/page.tsx b/src/app/(safespace)/space/phishing/assign/page.tsx index d13070a3..48910934 100644 --- a/src/app/(safespace)/space/phishing/assign/page.tsx +++ b/src/app/(safespace)/space/phishing/assign/page.tsx @@ -11,7 +11,7 @@ import clsx from "clsx"; import Robot from "@/components/robot/robot"; import Task from "@/components/task"; import { AchievementId } from "@/util/achievement-data"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { getUserService } from "@/services/user/UserService"; type Profile = { diff --git a/src/app/(safespace)/space/phishing/profiling/page.tsx b/src/app/(safespace)/space/phishing/profiling/page.tsx index bc9f2472..40b7168c 100644 --- a/src/app/(safespace)/space/phishing/profiling/page.tsx +++ b/src/app/(safespace)/space/phishing/profiling/page.tsx @@ -12,7 +12,7 @@ import clsx from "clsx"; import { HintCard } from "@/components/hint-card"; import Robot from "@/components/robot/robot"; import { AchievementId } from "@/util/achievement-data"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import Task from "@/components/task"; import { getUserService } from "@/services/user/UserService"; diff --git a/src/app/(safespace)/space/privatsphaere/perso/perso.tsx b/src/app/(safespace)/space/privatsphaere/perso/perso.tsx index eba943e5..8aa00206 100644 --- a/src/app/(safespace)/space/privatsphaere/perso/perso.tsx +++ b/src/app/(safespace)/space/privatsphaere/perso/perso.tsx @@ -1,6 +1,6 @@ import Button from "@/components/button"; import Checkbox from "@/components/checkbox"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { SetStateAction, useState } from "react"; diff --git a/src/app/(safespace)/space/privatsphaere/quiz/page.tsx b/src/app/(safespace)/space/privatsphaere/quiz/page.tsx index a3ddefea..7e4e7315 100644 --- a/src/app/(safespace)/space/privatsphaere/quiz/page.tsx +++ b/src/app/(safespace)/space/privatsphaere/quiz/page.tsx @@ -2,8 +2,8 @@ import { HintCard } from "@/components/hint-card"; import { useRouter } from "next/navigation"; import { useState } from "react"; -import QuizList from "@/components/Quiz/quiz-list"; -import { QuizParams } from "@/components/Quiz/quiz"; +import QuizList from "@/components/quiz/quiz-list"; +import { QuizParams } from "@/components/quiz/quiz"; import { AchievementId } from "@/util/achievement-data"; export default function StartGame() { diff --git a/src/app/(safespace)/space/rechte/dsgvo/page.tsx b/src/app/(safespace)/space/rechte/dsgvo/page.tsx index 945f4f9b..a48daff5 100644 --- a/src/app/(safespace)/space/rechte/dsgvo/page.tsx +++ b/src/app/(safespace)/space/rechte/dsgvo/page.tsx @@ -12,7 +12,7 @@ import clsx from "clsx"; import { HintCard } from "@/components/hint-card"; import Robot from "@/components/robot/robot"; import { AchievementId } from "@/util/achievement-data"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { Question } from "@phosphor-icons/react"; import Task from "@/components/task"; import { getUserService } from "@/services/user/UserService"; diff --git a/src/app/(safespace)/space/rechte/input/action.ts b/src/app/(safespace)/space/rechte/input/action.ts index e23ea26d..4d0732e3 100644 --- a/src/app/(safespace)/space/rechte/input/action.ts +++ b/src/app/(safespace)/space/rechte/input/action.ts @@ -1,13 +1,14 @@ import { AchievementId } from "@/util/achievement-data"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { getUserService } from "@/services/user/UserService"; +import { ApiEndpoints } from "@/lib/constants/endpoints"; -export default async function getFeedback( +export const getFeedback = async ( situation: string, userInput: string, solution: string, -): Promise { - const response = await fetch("/api/get-answer", { +): Promise => { + const response = await fetch(ApiEndpoints.ANSWERS, { method: "POST", headers: { "Content-Type": "application/json", @@ -18,7 +19,9 @@ export default async function getFeedback( throw new Error("Failed to fetch answer"); } return await response.json(); -} +}; + +export default getFeedback; export function extractScoreAndReason(str: string) { const trimmedStr = str.slice(1, -1); diff --git a/src/app/(safespace)/space/rechte/input/page.tsx b/src/app/(safespace)/space/rechte/input/page.tsx index 96df9746..1db3af5e 100644 --- a/src/app/(safespace)/space/rechte/input/page.tsx +++ b/src/app/(safespace)/space/rechte/input/page.tsx @@ -8,7 +8,7 @@ import getFeedback, { } from "@/app/(safespace)/space/rechte/input/action"; import { questions } from "@/util/rights/rights-data"; import { Score } from "@/app/(safespace)/space/rechte/input/score"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { useRouter } from "next/navigation"; export default function RightsInputValidation() { diff --git a/src/app/(safespace)/space/rechte/quiz/page.tsx b/src/app/(safespace)/space/rechte/quiz/page.tsx index e31cb977..84bd183c 100644 --- a/src/app/(safespace)/space/rechte/quiz/page.tsx +++ b/src/app/(safespace)/space/rechte/quiz/page.tsx @@ -1,13 +1,13 @@ "use client"; -import QuizList from "@/components/Quiz/quiz-list"; -import { QuizParams } from "@/components/Quiz/quiz"; +import QuizList from "@/components/quiz/quiz-list"; +import { QuizParams } from "@/components/quiz/quiz"; import { useRouter } from "next/navigation"; import { useState } from "react"; import Robot from "@/components/robot/robot"; import Button from "@/components/button"; import { AchievementId } from "@/util/achievement-data"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { PersistUserService } from "@/services/user/PersistUserService"; import { getUserService } from "@/services/user/UserService"; diff --git a/src/app/api/achievements/route.ts b/src/app/api/achievements/route.ts new file mode 100644 index 00000000..1fd572ee --- /dev/null +++ b/src/app/api/achievements/route.ts @@ -0,0 +1,112 @@ +import { NextRequest } from "next/server"; +import * as z from "zod"; +import { achievements, users } from "@/server/database/schema"; +import { and, eq } from "drizzle-orm"; +import { db } from "@/server/database/connection"; +import { + createSuccessResponse, + createCreatedResponse, +} from "@/lib/api/response"; +import { handleApiError, createErrorResponse } from "@/lib/api/error-handler"; +import { HttpStatus } from "@/lib/constants/http"; + +const getAchievementSchema = z.object({ + userId: z.coerce.number(), +}); + +const setAchievementSchema = z.object({ + userId: z.coerce.number(), + achievementEnum: z.string(), + unlocked: z.boolean(), +}); + +type GetAchievementRequest = z.infer; +type SetAchievementRequest = z.infer; + +type AchievementResponse = { + achievementEnum: string; + isAchieved: boolean; + userId: number; +}; + +export const GET = async (req: NextRequest) => { + try { + const searchParams = req.nextUrl.searchParams; + const userId = searchParams.get("userId"); + + if (!userId) { + return createErrorResponse("userId is required", HttpStatus.BAD_REQUEST); + } + + const { userId: validatedUserId } = getAchievementSchema.parse({ + userId, + }) as GetAchievementRequest; + + const userAchievements = await db + .select({ + achievementEnum: achievements.achievementEnum, + isAchieved: achievements.isAchieved, + userId: achievements.userId, + }) + .from(achievements) + .where(eq(achievements.userId, validatedUserId)); + + return createSuccessResponse(userAchievements); + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return createErrorResponse( + "Invalid request data", + HttpStatus.BAD_REQUEST, + ); + } + return handleApiError(error, "Failed to get achievements"); + } +}; + +export const POST = async (req: NextRequest) => { + try { + const requestData: unknown = await req.json(); + const { userId, achievementEnum, unlocked } = setAchievementSchema.parse( + requestData, + ) as SetAchievementRequest; + + const userData = await db.select().from(users).where(eq(users.id, userId)); + + if (userData.length === 0) { + return createErrorResponse("User not found", HttpStatus.NOT_FOUND); + } + + const foundAchievement = await db + .select() + .from(achievements) + .where( + and( + eq(achievements.userId, userId), + eq(achievements.achievementEnum, achievementEnum), + ), + ); + + if (foundAchievement.length > 0) { + return createErrorResponse( + "Achievement already set", + HttpStatus.BAD_REQUEST, + ); + } + + await db.insert(achievements).values({ + userId: userId, + isAchieved: unlocked, + achievementEnum: achievementEnum, + }); + + return createCreatedResponse({ message: "Achievement set" }); + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return createErrorResponse( + "Invalid request data", + HttpStatus.BAD_REQUEST, + ); + } + return handleApiError(error, "Failed to set achievement"); + } +}; diff --git a/src/app/api/get-answer/route.ts b/src/app/api/answers/route.ts similarity index 69% rename from src/app/api/get-answer/route.ts rename to src/app/api/answers/route.ts index f46697e2..bbb6e947 100644 --- a/src/app/api/get-answer/route.ts +++ b/src/app/api/answers/route.ts @@ -1,5 +1,8 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; import * as z from "zod"; +import { createSuccessResponse } from "@/lib/api/response"; +import { handleApiError, createErrorResponse } from "@/lib/api/error-handler"; +import { HttpStatus } from "@/lib/constants/http"; const requestSchema = z.object({ situation: z.string(), @@ -7,10 +10,22 @@ const requestSchema = z.object({ solution: z.string(), }); -export async function POST(rqq: NextRequest) { +type AnswerRequest = z.infer; + +type OpenAIResponse = { + choices: Array<{ + message: { + content: string; + }; + }>; +}; + +export const POST = async (req: NextRequest) => { try { - const _content = await rqq.json(); - const { situation, userInput, solution } = requestSchema.parse(_content); + const content: unknown = await req.json(); + const { situation, userInput, solution } = requestSchema.parse( + content, + ) as AnswerRequest; const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", @@ -29,7 +44,7 @@ export async function POST(rqq: NextRequest) { Frage: Ist das eine Verletzung deiner Rechte? User-Input: ${userInput} Lösung: ${solution} - Aufgabe: Vergleiche den User-Input mit der Lösung. Antwort mit "Ja" oder "Nein". + Aufgabe: Vergleiche den User-Input mit der Lösung. Antwort mit "Ja" oder "Nein". Fasse die Begründung kurz und bündig zusammen. Regeln: 1. Die Antwort sollte nicht länger als 3 Sätze sein. @@ -37,7 +52,7 @@ export async function POST(rqq: NextRequest) { 3. Bei einem "Ja", erkläre kurz, warum, und sprich den User direkt mit "DU" an. 4. Bewerte die Übereinstimmung mit der richtigen Antwort auf einer Skala von 1 bis 10, wobei 10 eine eindeutige Übereinstimmung bedeutet. - 5. Wenn die Antwort des Users mit der Lösung übereinstimmt, aber keine Begründung gegeben ist, + 5. Wenn die Antwort des Users mit der Lösung übereinstimmt, aber keine Begründung gegeben ist, vergib einen Score von 2. 6. Wenn die Antwort und Begründung vollständig mit der Lösung übereinstimmen, vergib einen Score von 10. @@ -56,19 +71,18 @@ export async function POST(rqq: NextRequest) { }); if (!response.ok) { - console.error("Failed to fetch answer", response); - throw new Error("Failed to fetch answer"); + throw new Error("Failed to fetch answer from OpenAI"); } - const answer = await response.json(); - return new NextResponse(JSON.stringify(answer.choices[0].message.content), { - status: 200, - statusText: "answer received", - }); + + const answer: OpenAIResponse = await response.json(); + return createSuccessResponse(answer.choices[0].message.content); } catch (error: unknown) { - console.error((error as Error).message); - return new NextResponse("Error", { - status: 400, - statusText: "Error", - }); + if (error instanceof z.ZodError) { + return createErrorResponse( + "Invalid request data", + HttpStatus.BAD_REQUEST, + ); + } + return handleApiError(error, "Failed to get answer"); } -} +}; diff --git a/src/app/api/create-user/route.ts b/src/app/api/create-user/route.ts deleted file mode 100644 index c575eb66..00000000 --- a/src/app/api/create-user/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { db } from "@/server/database/connection"; -import { HighScoreEnum, highScores, users } from "@/server/database/schema"; -import { NextRequest, NextResponse } from "next/server"; -import { eq } from "drizzle-orm"; -import * as z from "zod"; - -const requestSchema = z.object({ - username: z.string(), - mode: z.string(), - gameCode: z.string().nullable(), -}); - -async function insertDefaultHighScores(userId: number) { - // TODO: update the highScoreEnum if there is more highScores than the current one - for (let highScore in HighScoreEnum) { - await db.insert(highScores).values({ - userId: userId, - highScore: 0, - highScoreEnum: HighScoreEnum[highScore as keyof typeof HighScoreEnum], - }); - } -} - -export async function POST(req: NextRequest) { - try { - const requestData = await req.json(); - const { username, mode, gameCode } = requestSchema.parse(requestData); - - const existingUser = await db - .select({ - id: users.id, - name: users.userName, - }) - .from(users) - .where(eq(users.userName, username)); - - if (existingUser.length > 0) { - return new NextResponse("User already exists", { status: 409 }); - } - - if (mode === "singlePlayer") { - await db.insert(users).values({ userName: username, gameCode: null }); - } else if (mode === "multiPlayer") { - await db.insert(users).values({ userName: username, gameCode: gameCode }); - } - - const userData = await db - .select({ - id: users.id, - name: users.userName, - gameCode: users.gameCode, - }) - .from(users) - .where(eq(users.userName, username)); - - if (!userData || userData.length === 0) { - return new NextResponse("User not found", { status: 404 }); - } - - await insertDefaultHighScores(userData[0].id); - return NextResponse.json( - { userData: userData }, - { status: 201, statusText: "User created successfully" }, - ); - } catch (error: unknown) { - console.error("Database error:", error); - return new NextResponse(`Error: ${error}`, { status: 400 }); - } -} diff --git a/src/app/api/get-achievement/route.ts b/src/app/api/get-achievement/route.ts deleted file mode 100644 index 93f83d5c..00000000 --- a/src/app/api/get-achievement/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import * as z from "zod"; -import { achievements } from "@/server/database/schema"; -import { db } from "@/server/database/connection"; -import { eq } from "drizzle-orm"; - -const requestSchema = z.object({ - userId: z.coerce.number(), -}); - -export async function POST(req: NextRequest) { - try { - const request = await req.json(); - const { userId } = await requestSchema.parseAsync(request); - - const userAchievements = await db - .select({ - achievementEnum: achievements.achievementEnum, - isAchieved: achievements.isAchieved, - userId: achievements.userId, - }) - .from(achievements) - .where(eq(achievements.userId, userId)); - - if (!userAchievements) { - return new Response("Achievement not found", { - status: 404, - statusText: "Achievement not found", - }); - } - - return new NextResponse(JSON.stringify(userAchievements), { status: 200 }); - } catch (error) { - console.error("Error processing POST request", error); - return new Response("Error", { - status: 500, - statusText: "Internal Server Error", - }); - } -} diff --git a/src/app/api/get-highScores/route.ts b/src/app/api/get-highScores/route.ts deleted file mode 100644 index dc051327..00000000 --- a/src/app/api/get-highScores/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import * as z from "zod"; -import { HighScoreEnum, highScores } from "@/server/database/schema"; -import { and, eq } from "drizzle-orm"; -import { db } from "@/server/database/connection"; - -const requestSchema = z.object({ - userId: z.coerce.number(), - highScoreEnum: z.nativeEnum(HighScoreEnum), -}); - -export async function POST(req: NextRequest) { - try { - const requestBody = await req.json(); - const { userId, highScoreEnum } = requestSchema.parse(requestBody); - - const highScore = await db - .select({ - highScore: highScores.highScore, - }) - .from(highScores) - .where( - and( - eq(highScores.userId, userId), - eq(highScores.highScoreEnum, highScoreEnum), - ), - ); - - if (highScore.length === 0) { - return new NextResponse("No highscores found", { - status: 404, - statusText: "No highscores found", - }); - } - - return new NextResponse(JSON.stringify(highScore[0].highScore), { - status: 200, - statusText: "highscores data arrived", - }); - } catch (error) { - console.error("Error processing POST request", error); - return new NextResponse("Error", { - status: 500, - statusText: "Internal Server Error", - }); - } -} diff --git a/src/app/api/get-user/route.ts b/src/app/api/get-user/route.ts deleted file mode 100644 index 09e52901..00000000 --- a/src/app/api/get-user/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import * as z from "zod"; -import { users } from "@/server/database/schema"; -import { db } from "@/server/database/connection"; -import { eq } from "drizzle-orm"; - -const requestSchema = z.object({ - userId: z.coerce.number(), -}); -export async function POST(req: NextRequest) { - const requestBody = await req.json(); - - const { userId } = requestSchema.parse(requestBody); - - try { - const userData = await db - .select({ - id: users.id, - userName: users.userName, - gameCode: users.gameCode, - }) - .from(users) - .where(eq(users.id, userId)); - - return new NextResponse(JSON.stringify(userData[0]), { - status: 200, - statusText: "User data arrived", - }); - } catch (error: unknown) { - console.error("Error setting achievement:", error); - - if (error instanceof z.ZodError) { - return new NextResponse("Invalid request data", { - status: 400, - statusText: "Invalid request data", - }); - } - - return new NextResponse("Internal Server Error", { - status: 500, - statusText: "Internal Server Error", - }); - } -} diff --git a/src/app/api/highscores/route.ts b/src/app/api/highscores/route.ts new file mode 100644 index 00000000..20c9336d --- /dev/null +++ b/src/app/api/highscores/route.ts @@ -0,0 +1,104 @@ +import { NextRequest } from "next/server"; +import * as z from "zod"; +import { HighScoreEnum, highScores } from "@/server/database/schema"; +import { and, eq } from "drizzle-orm"; +import { db } from "@/server/database/connection"; +import { createSuccessResponse } from "@/lib/api/response"; +import { handleApiError, createErrorResponse } from "@/lib/api/error-handler"; +import { HttpStatus } from "@/lib/constants/http"; + +const getHighScoreSchema = z.object({ + userId: z.coerce.number(), + highScoreEnum: z.nativeEnum(HighScoreEnum), +}); + +const setHighScoreSchema = z.object({ + userId: z.coerce.number(), + highScore: z.number(), + highScoreEnum: z.nativeEnum(HighScoreEnum), +}); + +type GetHighScoreRequest = z.infer; +type SetHighScoreRequest = z.infer; + +type HighScoreResponse = { + highScore: number; +}; + +export const GET = async (req: NextRequest) => { + try { + const searchParams = req.nextUrl.searchParams; + const userId = searchParams.get("userId"); + const highScoreEnum = searchParams.get("highScoreEnum"); + + if (!userId || !highScoreEnum) { + return createErrorResponse( + "userId and highScoreEnum are required", + HttpStatus.BAD_REQUEST, + ); + } + + const { userId: validatedUserId, highScoreEnum: validatedEnum } = + getHighScoreSchema.parse({ + userId, + highScoreEnum, + }) as GetHighScoreRequest; + + const highScore = await db + .select({ + highScore: highScores.highScore, + }) + .from(highScores) + .where( + and( + eq(highScores.userId, validatedUserId), + eq(highScores.highScoreEnum, validatedEnum), + ), + ); + + if (highScore.length === 0) { + return createErrorResponse("No highscores found", HttpStatus.NOT_FOUND); + } + + return createSuccessResponse({ + highScore: highScore[0].highScore, + }); + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return createErrorResponse( + "Invalid request data", + HttpStatus.BAD_REQUEST, + ); + } + return handleApiError(error, "Failed to get highscores"); + } +}; + +export const PUT = async (req: NextRequest) => { + try { + const requestData: unknown = await req.json(); + const { userId, highScore, highScoreEnum } = setHighScoreSchema.parse( + requestData, + ) as SetHighScoreRequest; + + await db + .update(highScores) + .set({ highScore: highScore }) + .where( + and( + eq(highScores.userId, userId), + eq(highScores.highScoreEnum, highScoreEnum), + ), + ); + + return createSuccessResponse({ message: "Highscore set" }); + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return createErrorResponse( + "Invalid request data", + HttpStatus.BAD_REQUEST, + ); + } + return handleApiError(error, "Failed to set highscore"); + } +}; diff --git a/src/app/api/leaderboard/route.ts b/src/app/api/leaderboard/route.ts index 1d804159..b73accd5 100644 --- a/src/app/api/leaderboard/route.ts +++ b/src/app/api/leaderboard/route.ts @@ -1,23 +1,32 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; +import * as z from "zod"; import { db } from "@/server/database/connection"; import { achievements, users } from "@/server/database/schema"; import { and, eq, sql } from "drizzle-orm"; +import { createSuccessResponse } from "@/lib/api/response"; +import { handleApiError, createErrorResponse } from "@/lib/api/error-handler"; +import { HttpStatus } from "@/lib/constants/http"; + +const requestSchema = z.object({ + gameCode: z.string(), +}); + +type LeaderboardRequest = z.infer; type UserData = { username: string; score: number; }; -export async function POST(req: NextRequest) { - try { - const { gameCode } = await req.json(); +type LeaderboardEntry = { + name: string; + score: number; +}; - if (gameCode === undefined) { - return new NextResponse("Error", { - status: 404, - statusText: "gameCode is required. Your game code was undefined", - }); - } +export const POST = async (req: NextRequest) => { + try { + const requestData: unknown = await req.json(); + const { gameCode } = requestSchema.parse(requestData) as LeaderboardRequest; const userData = await db .select({ @@ -38,27 +47,24 @@ export async function POST(req: NextRequest) { .orderBy(sql`score DESC`); if (!userData || userData.length === 0) { - console.error("No user data found for gameCode", gameCode); - return new NextResponse("Error", { - status: 404, - statusText: "No user data found", - }); + return createErrorResponse("No user data found", HttpStatus.NOT_FOUND); } - const leaderboardData = userData.map((user: UserData) => ({ - name: user.username, - score: user.score, - })); + const leaderboardData: LeaderboardEntry[] = userData.map( + (user: UserData) => ({ + name: user.username, + score: user.score, + }), + ); - return new NextResponse(JSON.stringify(leaderboardData), { - status: 200, - statusText: "leaderboard user data arrived", - }); - } catch (error) { - console.error("Error processing POST request", error); - return new NextResponse("Error", { - status: 500, - statusText: "Internal Server Error", - }); + return createSuccessResponse(leaderboardData); + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return createErrorResponse( + "Invalid request data", + HttpStatus.BAD_REQUEST, + ); + } + return handleApiError(error, "Failed to get leaderboard data"); } -} +}; diff --git a/src/app/api/set-achievement/route.ts b/src/app/api/set-achievement/route.ts deleted file mode 100644 index 52f8d7fb..00000000 --- a/src/app/api/set-achievement/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import * as z from "zod"; -import { achievements, users } from "@/server/database/schema"; -import { and, eq } from "drizzle-orm"; -import { db } from "@/server/database/connection"; - -const requestSchema = z.object({ - userId: z.coerce.number(), - achievementEnum: z.string(), - unlocked: z.boolean(), -}); - -export async function POST(req: NextRequest) { - try { - const requestData = await req.json(); - const { userId, achievementEnum, unlocked } = - requestSchema.parse(requestData); - const userData = await db.select().from(users).where(eq(users.id, userId)); - - if (userData.length === 0) { - return new NextResponse("User not found", { - status: 404, - statusText: "User not found", - }); - } - - const foundAchievement = await db - .select() - .from(achievements) - .where( - and( - eq(achievements.userId, userId), - eq(achievements.achievementEnum, achievementEnum), - ), - ); - - if (foundAchievement.length > 0) { - return new NextResponse("Achievement already set", { - status: 400, - statusText: "Achievement already set", - }); - } - - await db.insert(achievements).values({ - userId: userId, - isAchieved: unlocked, - achievementEnum: achievementEnum, - }); - - return new NextResponse("Achievement set", { - status: 200, - statusText: "Achievement set", - }); - } catch (error) { - console.error("Error setting achievement:", error); - - if (error instanceof z.ZodError) { - return new NextResponse("Invalid request data", { - status: 400, - statusText: "Invalid request data", - }); - } - - return new NextResponse("Internal Server Error", { - status: 500, - statusText: "Internal Server Error", - }); - } -} diff --git a/src/app/api/set-highScore/route.ts b/src/app/api/set-highScore/route.ts deleted file mode 100644 index cfa1c3e3..00000000 --- a/src/app/api/set-highScore/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import * as z from "zod"; -import { db } from "@/server/database/connection"; -import { HighScoreEnum, highScores } from "@/server/database/schema"; -import { and, eq } from "drizzle-orm"; - -const requestSchema = z.object({ - userId: z.coerce.number(), - highScore: z.number(), - highScoreEnum: z.nativeEnum(HighScoreEnum), -}); - -export async function POST(req: NextRequest) { - const requestData = await req.json(); - const { userId, highScore, highScoreEnum } = requestSchema.parse(requestData); - - try { - await db - .update(highScores) - .set({ highScore: highScore }) - .where( - and( - eq(highScores.userId, userId), - eq(highScores.highScoreEnum, highScoreEnum), - ), - ); - return new NextResponse("Highscore set", { - status: 200, - statusText: "The highscore was set", - }); - } catch (error) { - console.error("Error setting Highscore:", error); - return new NextResponse("Error", { - status: 500, - statusText: "Internal Server Error", - }); - } -} diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts new file mode 100644 index 00000000..435e42c5 --- /dev/null +++ b/src/app/api/users/route.ts @@ -0,0 +1,136 @@ +import { db } from "@/server/database/connection"; +import { HighScoreEnum, highScores, users } from "@/server/database/schema"; +import { NextRequest } from "next/server"; +import { eq } from "drizzle-orm"; +import * as z from "zod"; +import { + createSuccessResponse, + createCreatedResponse, +} from "@/lib/api/response"; +import { handleApiError, createErrorResponse } from "@/lib/api/error-handler"; +import { HttpStatus } from "@/lib/constants/http"; + +const createUserSchema = z.object({ + username: z.string(), + mode: z.string(), + gameCode: z.string().nullable(), +}); + +const getUserSchema = z.object({ + userId: z.coerce.number(), +}); + +type CreateUserRequest = z.infer; +type GetUserRequest = z.infer; + +type UserResponse = { + id: number; + name: string; + gameCode: string | null; +}; + +type CreateUserResponse = { + userData: UserResponse[]; +}; + +const insertDefaultHighScores = async (userId: number): Promise => { + for (const highScore in HighScoreEnum) { + await db.insert(highScores).values({ + userId: userId, + highScore: 0, + highScoreEnum: HighScoreEnum[highScore as keyof typeof HighScoreEnum], + }); + } +}; + +export const POST = async (req: NextRequest) => { + try { + const requestData: unknown = await req.json(); + const { username, mode, gameCode } = createUserSchema.parse( + requestData, + ) as CreateUserRequest; + + const existingUser = await db + .select({ + id: users.id, + name: users.userName, + }) + .from(users) + .where(eq(users.userName, username)); + + if (existingUser.length > 0) { + return createErrorResponse("User already exists", HttpStatus.BAD_REQUEST); + } + + if (mode === "singlePlayer") { + await db.insert(users).values({ userName: username, gameCode: null }); + } else if (mode === "multiPlayer") { + await db.insert(users).values({ userName: username, gameCode: gameCode }); + } + + const userData = await db + .select({ + id: users.id, + name: users.userName, + gameCode: users.gameCode, + }) + .from(users) + .where(eq(users.userName, username)); + + if (!userData || userData.length === 0) { + return createErrorResponse("User not found", HttpStatus.NOT_FOUND); + } + + await insertDefaultHighScores(userData[0].id); + + return createCreatedResponse({ + userData: userData, + }); + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return createErrorResponse( + "Invalid request data", + HttpStatus.BAD_REQUEST, + ); + } + return handleApiError(error, "Failed to create user"); + } +}; + +export const GET = async (req: NextRequest) => { + try { + const searchParams = req.nextUrl.searchParams; + const userId = searchParams.get("userId"); + + if (!userId) { + return createErrorResponse("userId is required", HttpStatus.BAD_REQUEST); + } + + const { userId: validatedUserId } = getUserSchema.parse({ + userId, + }) as GetUserRequest; + + const userData = await db + .select({ + id: users.id, + userName: users.userName, + gameCode: users.gameCode, + }) + .from(users) + .where(eq(users.id, validatedUserId)); + + if (!userData || userData.length === 0) { + return createErrorResponse("User not found", HttpStatus.NOT_FOUND); + } + + return createSuccessResponse(userData[0]); + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return createErrorResponse( + "Invalid request data", + HttpStatus.BAD_REQUEST, + ); + } + return handleApiError(error, "Failed to get user"); + } +}; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f1d6790b..54b156ad 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,8 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; -import { MessageProvider } from "@/services/notfication/message-provider"; -import NotificationsProvider from "@/services/notfication/notifications-provider"; +import { MessageProvider } from "@/services/notification/message-provider"; +import NotificationsProvider from "@/services/notification/notifications-provider"; import React from "react"; const inter = Inter({ subsets: ["latin"] }); diff --git a/src/app/page.tsx b/src/app/page.tsx index 6cd28466..b6a69504 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,8 +2,8 @@ import Button from "@/components/button"; import { redirect, useRouter } from "next/navigation"; -import { SinglePlayer } from "@/components/LandingPage/single-player"; -import { Multiplayer } from "@/components/LandingPage/multi-player"; +import { SinglePlayer } from "@/components/landing-page/single-player"; +import { Multiplayer } from "@/components/landing-page/multi-player"; import { useEffect, useState } from "react"; import { adjectives, diff --git a/src/components/Quiz/helper.ts b/src/components/Quiz/helper.ts index 3b225521..658f191b 100644 --- a/src/components/Quiz/helper.ts +++ b/src/components/Quiz/helper.ts @@ -1,5 +1,5 @@ import { TCanvasConfettiAnimationOptions } from "react-canvas-confetti/src/types"; -import { QuizParams } from "@/components/Quiz/quiz"; +import { QuizParams } from "@/components/quiz/quiz"; import { AchievementId } from "@/util/achievement-data"; import React from "react"; @@ -68,11 +68,11 @@ export function onSelect( ) { setQuizzesState( quizzesState.map((state, index) => { - if (quizIndex == index) { + if (quizIndex === index) { return { quiz: state.quiz, selection: selection, - isSolved: selection == state.quiz.correctAnswer, + isSolved: selection === state.quiz.correctAnswer, isDone: isDone, }; } else { diff --git a/src/components/Quiz/quiz-list.tsx b/src/components/Quiz/quiz-list.tsx index 77e1b101..6e848792 100644 --- a/src/components/Quiz/quiz-list.tsx +++ b/src/components/Quiz/quiz-list.tsx @@ -3,10 +3,10 @@ import { useEffect, useState } from "react"; import clsx from "clsx"; import Button from "@/components/button"; import { Achievement, AchievementData } from "@/util/achievement-data"; -import AchievementCard from "@/components/Achievements/achievement-card"; +import AchievementCard from "@/components/achievements/achievement-card"; import { PersistUserService } from "@/services/user/PersistUserService"; import Explosion from "react-canvas-confetti/dist/presets/explosion"; -import Quiz, { QuizParams } from "@/components/Quiz/quiz"; +import Quiz, { QuizParams } from "@/components/quiz/quiz"; import { customDecorateOptions, getAchievedScore, @@ -14,7 +14,7 @@ import { QuizListProps, quizStateClasses, QuizzesStateProps, -} from "@/components/Quiz/helper"; +} from "@/components/quiz/helper"; import { getUserService } from "@/services/user/UserService"; export default function QuizList({ @@ -48,12 +48,12 @@ export default function QuizList({ } if (achievement !== undefined && currentQuizIndex === quizzes.length - 1) { - unlockAchievement(achievement).then((res) => console.log(res)); + unlockAchievement(achievement); } }, [achievement, achievedScore]); function nextQuiz() { - if (currentQuizIndex == quizzes.length - 1) return; + if (currentQuizIndex === quizzes.length - 1) return; setCurrentQuizIndex(currentQuizIndex + 1); onNextQuestion?.(); } diff --git a/src/components/Quiz/quiz.tsx b/src/components/Quiz/quiz.tsx index aa2c30f9..9544aec6 100644 --- a/src/components/Quiz/quiz.tsx +++ b/src/components/Quiz/quiz.tsx @@ -17,41 +17,42 @@ export type QuizParams = { showCorrectAnswer?: boolean; }; -export default function Quiz(quiz: QuizParams) { +export const Quiz = (quiz: QuizParams) => { const [selection, setSelection] = useState(undefined); - function onClick(index: number) { + const onClick = (index: number) => { if (quiz.onAnswerClick) { quiz.onAnswerClick(); } - if (quiz.showCorrectAnswer && selection != undefined) return; + if (quiz.showCorrectAnswer && selection !== undefined) return; setSelection(index); const isDone = quiz.showCorrectAnswer - ? index != undefined - : index == quiz.correctAnswer; + ? index !== undefined + : index === quiz.correctAnswer; quiz.onSelect?.(index, isDone); - } + }; - function buttonStyle(index: number): ButtonStyle { + const buttonStyle = (index: number): ButtonStyle => { if ( quiz.showCorrectAnswer && - selection != undefined && - index == quiz.correctAnswer + selection !== undefined && + index === quiz.correctAnswer ) { return "green"; } else { - return selection == index - ? index == quiz.correctAnswer + return selection === index + ? index === quiz.correctAnswer ? "green" : "red" : "default"; } - } - function showCursorClass() { - return quiz.showCorrectAnswer && selection != undefined + }; + + const showCursorClass = () => { + return quiz.showCorrectAnswer && selection !== undefined ? "cursor-auto" : null; - } + }; return (
@@ -72,9 +73,9 @@ export default function Quiz(quiz: QuizParams) { ))}
- {selection != undefined && ( + {selection !== undefined && (
- {selection == quiz.correctAnswer ? ( + {selection === quiz.correctAnswer ? ( <>

Richtige Antwort! @@ -108,4 +109,6 @@ export default function Quiz(quiz: QuizParams) { )}

); -} +}; + +export default Quiz; diff --git a/src/components/button.tsx b/src/components/button.tsx index 7ef65959..3d1bc0a5 100644 --- a/src/components/button.tsx +++ b/src/components/button.tsx @@ -13,28 +13,28 @@ type ButtonProps = { type?: "submit" | "button" | undefined; } & PropsWithChildren; -export default function Button({ +export const Button = ({ children, onClick, className, style, disabled, type, -}: ButtonProps) { +}: ButtonProps) => { const buttonBase = "inline-flex justify-center items-center align-center px-6 py-3 font-medium rounded-2xl transition-colors"; let buttonStyle = "bg-orange-500 text-white"; let buttonHoverStyle = "hover:bg-orange-600"; - if (style == "green") { + if (style === "green") { buttonStyle = "bg-lime-500 text-white"; buttonHoverStyle = "hover:bg-lime-600"; - } else if (style == "red") { + } else if (style === "red") { buttonStyle = "bg-red-500 text-white"; buttonHoverStyle = "hover:bg-red-600"; - } else if (style == "secondary") { + } else if (style === "secondary") { buttonStyle = "text-[#004F86] bg-transparent border-2 border-sky-100"; buttonHoverStyle = "hover:bg-sky-100"; - } else if (style == "neutral") { + } else if (style === "neutral") { buttonStyle = "bg-gray-500 text-white"; buttonHoverStyle = "hover:bg-gray-600"; } @@ -57,4 +57,6 @@ export default function Button({ {children} ); -} +}; + +export default Button; diff --git a/src/components/exercise-navigation.tsx b/src/components/exercise-navigation.tsx index e4489963..91bd698c 100644 --- a/src/components/exercise-navigation.tsx +++ b/src/components/exercise-navigation.tsx @@ -83,7 +83,7 @@ export function ExerciseNavigation() { if ( !achievements.find( - (a) => a.achievementEnum == AchievementId.INTRO_FINISHED, + (a) => a.achievementEnum === AchievementId.INTRO_FINISHED, ) ) { setModulesUnlocked(false); diff --git a/src/components/hint-card.tsx b/src/components/hint-card.tsx index ec81b6c8..41d42968 100644 --- a/src/components/hint-card.tsx +++ b/src/components/hint-card.tsx @@ -3,7 +3,7 @@ import Button from "./button"; import React, { useState } from "react"; import clsx from "clsx"; -type ActionCardProps = { +type HintCardProps = { text: string; buttonText: string; iconSrc?: string; @@ -17,7 +17,7 @@ export function HintCard({ iconSrc, hint, className, -}: ActionCardProps) { +}: HintCardProps) { const [showHint, setShowHint] = useState(false); return (
{profile.description}

- {profile.posts.length == 0 ? ( + {profile.posts.length === 0 ? (
(
- {activeSection == sectionName && ( + {activeSection === sectionName && (
void; diff --git a/src/components/privacy-quiz.tsx b/src/components/privacy-quiz.tsx index 9b329017..ee9e6c8f 100644 --- a/src/components/privacy-quiz.tsx +++ b/src/components/privacy-quiz.tsx @@ -7,7 +7,7 @@ import { useRouter } from "next/navigation"; import { PrivacyQuizQuestion } from "@/app/(safespace)/space/privatsphaere/swipe/page"; import Robot from "@/components/robot/robot"; import { AchievementId } from "@/util/achievement-data"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { getUserService } from "@/services/user/UserService"; export function PrivacyQuiz({ @@ -32,7 +32,7 @@ export function PrivacyQuiz({ const handleNextQuestion = () => { setAnswer(null); setCurrentQuestionIndex((prevIndex) => { - if (prevIndex + 1 == questions.length) { + if (prevIndex + 1 === questions.length) { const userService = getUserService(); userService .setAchievement(AchievementId.PRIVATSPHAERE_FINISHED, true) @@ -137,7 +137,7 @@ function QuizResult({ - {userAnswer == currentQuestion.isPersonenbezogen + {userAnswer === currentQuestion.isPersonenbezogen ? "Richtig! " : "Leider Falsch. "} diff --git a/src/components/progress-bar.tsx b/src/components/progress-bar.tsx index ccc303d9..c54eddb8 100644 --- a/src/components/progress-bar.tsx +++ b/src/components/progress-bar.tsx @@ -38,8 +38,8 @@ export function ProgressBar({ ); const style = { left: - section.progress == 1 ? undefined : section.progress * 100 + "%", - right: section.progress == 1 ? "0" : undefined, + section.progress === 1 ? undefined : section.progress * 100 + "%", + right: section.progress === 1 ? "0" : undefined, }; const textClasses = clsx( "absolute text-nowrap text-center text-slate-600 text-sm transition-all duration-200", diff --git a/src/components/robot-in-passwort.tsx b/src/components/robot-in-passwort.tsx index 2036fdae..70326454 100644 --- a/src/components/robot-in-passwort.tsx +++ b/src/components/robot-in-passwort.tsx @@ -6,7 +6,7 @@ import clsx from "clsx"; import { CSSProperties, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { AchievementId } from "@/util/achievement-data"; -import { useMessages } from "@/services/notfication/message-provider"; +import { useMessages } from "@/services/notification/message-provider"; import { getUserService } from "@/services/user/UserService"; type RobotIntroductionProps = { @@ -78,7 +78,7 @@ export default function RobotInPasswort({
); } - From 1b61f27d5392ccafff49f3383ca224035026db6e Mon Sep 17 00:00:00 2001 From: uwumarogie Date: Mon, 24 Nov 2025 14:21:34 +0100 Subject: [PATCH 5/5] feat: add achievement card and quiz components with state management - Introduced AchievementCard component for displaying achievements. - Added Quiz and QuizList components to manage quiz functionality and state. - Implemented helper functions for quiz state management and confetti animation options. - Enhanced user experience with achievement unlocking and summary display after quizzes. --- .../{Achievements => achievements}/achievement-card.tsx | 0 src/components/{Quiz => quiz}/helper.ts | 0 src/components/{Quiz => quiz}/quiz-list.tsx | 0 src/components/{Quiz => quiz}/quiz.tsx | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/components/{Achievements => achievements}/achievement-card.tsx (100%) rename src/components/{Quiz => quiz}/helper.ts (100%) rename src/components/{Quiz => quiz}/quiz-list.tsx (100%) rename src/components/{Quiz => quiz}/quiz.tsx (100%) diff --git a/src/components/Achievements/achievement-card.tsx b/src/components/achievements/achievement-card.tsx similarity index 100% rename from src/components/Achievements/achievement-card.tsx rename to src/components/achievements/achievement-card.tsx diff --git a/src/components/Quiz/helper.ts b/src/components/quiz/helper.ts similarity index 100% rename from src/components/Quiz/helper.ts rename to src/components/quiz/helper.ts diff --git a/src/components/Quiz/quiz-list.tsx b/src/components/quiz/quiz-list.tsx similarity index 100% rename from src/components/Quiz/quiz-list.tsx rename to src/components/quiz/quiz-list.tsx diff --git a/src/components/Quiz/quiz.tsx b/src/components/quiz/quiz.tsx similarity index 100% rename from src/components/Quiz/quiz.tsx rename to src/components/quiz/quiz.tsx