Skip to content

(SP: 10) [Frontend] Quiz flow: Security encryption + session persistence + UX fixes#131

Merged
ViktorSvertoka merged 5 commits into
developfrom
sl/feat/quiz
Jan 12, 2026
Merged

(SP: 10) [Frontend] Quiz flow: Security encryption + session persistence + UX fixes#131
ViktorSvertoka merged 5 commits into
developfrom
sl/feat/quiz

Conversation

@LesiaUKR
Copy link
Copy Markdown
Collaborator

@LesiaUKR LesiaUKR commented Jan 12, 2026

Week 5

Summary

Implementation of Quiz features: security improvements, session persistence, and UX fixes.

Closes

Changes

#130: Security - Encrypted Answers (SP: 3)

  • lib/quiz/quiz-crypto.ts - AES-256-GCM encryption utilities
  • app/api/quiz/verify-answer/route.ts - Server-side answer verification endpoint
  • db/queries/quiz.ts - Client-safe types (QuizQuestionClient, QuizAnswerClient)

#124: Session Persistence + Quit Confirmation (SP: 4)

  • lib/quiz/quiz-session.ts - localStorage session utilities
  • hooks/useQuizSession.ts - Session persistence hook
  • hooks/useQuizGuards.ts - Browser navigation guards
  • components/ui/confirm-modal.tsx - Reusable confirmation dialog

#122: Language Switch Category Fix (SP: 1)

  • components/quiz/QuizzesSection.tsx - URL params sync for category

#117: QuizCard Height (SP: 1)

  • components/quiz/QuizCard.tsx - Consistent card height with flex layout

#115: Dashboard Link Fix (SP: 1)

  • Fixed Continue Learning button to link correctly

Test plan

  • DevTools Network tab shows no isCorrect in quiz response
  • Answer verification works via server API
  • Language switch mid-quiz preserves progress
  • Exit Quiz button shows custom modal
  • Browser back during quiz shows confirmation
  • Category preserved on language switch (/quizzes page)
  • QuizCard heights are consistent
  • Dashboard Continue Learning links to correct page

Environment

  • Requires QUIZ_ENCRYPTION_KEY env variable (32-byte hex string)

Summary by CodeRabbit

  • New Features

    • Quiz sessions are now automatically saved and can be resumed if interrupted.
    • Added exit quiz functionality with confirmation to prevent accidental data loss.
    • Quiz category filters now persist in the URL for easier navigation.
    • Incomplete quizzes show a dedicated status indicator and message.
  • Improvements

    • Answer verification moved to server for enhanced security.
    • Quiz progress cards now display completion status and clearer progress metrics.
    • Language switching preserves quiz session data.

✏️ Tip: You can customize this high-level summary in your review settings.

…z_attempts

- Change "Start a Quiz" link to /quizzes (for users without attempts)
- Change "Continue Learning" link to /q&a (for users with attempts)
- Add CASCADE delete constraint on quiz_attempts.userId foreign key
Issue #130 - Security: Encrypted Answers
- Add AES-256-GCM encryption for quiz answers (quiz-crypto.ts)
- Create /api/quiz/verify-answer endpoint for server-side validation
- Add client-safe types (QuizQuestionClient, QuizAnswerClient)
- Remove isCorrect from client response to prevent DevTools cheating

Issue #124 - Session Persistence + Quit Confirmation
- Add localStorage session persistence (useQuizSession hook)
- Add quit confirmation with custom ConfirmModal
- Add browser navigation guards (useQuizGuards hook)
- Support language switch without losing progress

Issue #122 - Language Switch Category Fix
- Sync category selection to URL params in QuizzesSection
- Preserve query params on language switch

Additional fixes:
- Add isIncomplete state for timeout scenarios
- Reorganize quiz utils into lib/quiz/ directory
@netlify
Copy link
Copy Markdown

netlify Bot commented Jan 12, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit 1b62dd4
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/696557547c12e90007715f07
😎 Deploy Preview https://deploy-preview-131--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 12, 2026

📝 Walkthrough

Walkthrough

This PR implements quiz session persistence, server-side answer verification, and quiz exit functionality. It introduces AES-256-GCM encryption for answer protection, localStorage-based session state recovery, an exit confirmation modal, client-safe question rendering by removing isCorrect details, and URL-based category filtering. Database schema adds foreign key constraints and indexes on quiz_attempts. Multiple new hooks and utilities support session management and quiz guards.

Changes

Cohort / File(s) Summary
Import Path Reorganization
frontend/app/[locale]/login/page.tsx, frontend/app/[locale]/signup/page.tsx, frontend/components/quiz/PendingResultHandler.tsx
Updated imports for getPendingQuizResult and clearPendingQuizResult from @/lib/guest-quiz to @/lib/quiz/guest-quiz (module reorganization).
Quiz Session & Persistence
frontend/hooks/useQuizSession.ts, frontend/lib/quiz/quiz-session.ts, frontend/lib/quiz/quiz-storage-keys.ts
New modules for localStorage-based quiz session persistence with 30-minute TTL, session restoration on mount, and storage key utilities including reload and allow-restore flags.
Quiz Guards & Exit Flow
frontend/hooks/useQuizGuards.ts, frontend/components/ui/confirm-modal.tsx
New hook to guard in-progress quizzes from accidental exits via beforeunload, popstate, and link click interception. New ConfirmModal component for quit confirmation with variant styling.
Quiz Page & Container
frontend/app/[locale]/quiz/[slug]/page.tsx, frontend/components/quiz/QuizContainer.tsx
Quiz page now accepts searchParams with optional seed; uses encryption integration (createEncryptedAnswersBlob, stripCorrectAnswers) and passes encrypted answers and seed to container. Container refactored to integrate session restoration, server-side answer verification via /api/quiz/verify-answer, exit modal, and dispatch RESTORE_SESSION action. Extended state with isIncomplete flag and new reducer cases.
Answer Encryption & Verification
frontend/lib/quiz/quiz-crypto.ts, frontend/app/api/quiz/verify-answer/route.ts
New AES-256-GCM encryption module with encryptAnswers, decryptAnswers, and createEncryptedAnswersBlob. New API endpoint validates encrypted blob, decrypts, looks up correct answer, and returns isCorrect boolean.
Question & Result Components
frontend/components/quiz/QuizQuestion.tsx, frontend/components/quiz/QuizResult.tsx
QuizQuestion updated to accept isCorrect prop (from server verification) instead of deriving from question.answers. QuizResult adds optional isIncomplete prop; shows incomplete-attempt message before standard feedback when true.
Card & Quiz Discovery
frontend/components/quiz/QuizCard.tsx, frontend/components/quiz/QuizzesSection.tsx
QuizCard converts to flex layout with flex-grow, adds "completed" badge, applies line-clamp to description, and consolidates progress display into single-line container. QuizzesSection switches from internal state to URL-driven category filtering via useSearchParams and useRouter; defaults to 'git' category.
Language & UI
frontend/components/shared/LanguageSwitcher.tsx, frontend/db/queries/quiz.ts
LanguageSwitcher preserves query parameters and sets session flag (quiz-allow-restore) when on quiz page. quiz.ts introduces client DTOs (QuizAnswerClient, QuizQuestionClient) and stripCorrectAnswers utility to omit correctness from client-facing questions.
Database Schema & Migrations
frontend/db/schema/quiz.ts, frontend/drizzle/0013_brown_gamora.sql, frontend/drizzle/0015_dear_legion.sql, frontend/drizzle/meta/0013_snapshot.json, frontend/drizzle/meta/_journal.json
Added foreign key on quiz_attempts.userId referencing users.id with cascade delete and indexes on user_id and quiz_id. Snapshot updated with new constraints and metadata.
Localization
frontend/messages/en.json, frontend/messages/pl.json, frontend/messages/uk.json
Added exitModal object under quiz.section with title, message, confirm, and cancel translations for exit confirmation.
Navigation & Stats
frontend/components/dashboard/StatsCard.tsx
Updated button navigation: "Start a Quiz" → /quizzes, "Continue Learning" → /qa; added aria-hidden="true" to gradient overlay.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client as QuizContainer
    participant API as /api/quiz/verify-answer
    participant Crypto as Decryption
    participant DB as Database/Query

    User->>Client: Selects answer
    Client->>API: POST {questionId, answerId, encryptedAnswers}
    API->>Crypto: decryptAnswers(encryptedBlob)
    Crypto-->>API: CorrectAnswersMap
    API->>DB: Lookup correctAnswerId for questionId
    DB-->>API: correctAnswerId
    API->>API: Compare answerId === correctAnswerId
    API-->>Client: {isCorrect: boolean}
    Client->>Client: Dispatch ANSWER_SELECTED with isCorrect
    Client->>User: Show feedback (correct/incorrect)
Loading
sequenceDiagram
    participant User
    participant Component as QuizContainer
    participant Hook as useQuizSession
    participant Storage as localStorage
    participant Guard as useQuizGuards

    Component->>Hook: Mount with quizId
    Hook->>Storage: loadQuizSession(quizId)
    Storage-->>Hook: QuizSessionData (if exists & valid)
    Hook->>Component: onRestore callback
    Component->>Component: Dispatch RESTORE_SESSION
    Component->>Guard: Register guards (beforeunload, popstate)
    
    User->>Component: Answers questions
    Component->>Hook: State updated
    Hook->>Storage: saveQuizSession(quizId, state)
    Storage-->>Hook: Saved ✓

    User->>Guard: Navigates away / language switch
    Guard->>Guard: Detect in_progress + exit
    Guard->>Component: Trigger onExit (markQuitting)
    Component->>Component: Show ConfirmModal
    
    User->>Component: Confirm exit
    Component->>Storage: clearQuizSession(quizId)
    Component->>User: Redirect to /quizzes
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka

Poem

🐰 A quiz guard hops into the ring,
Sessions saved, encryption's wing,
No cheating now, the answers hide—
Exit clean with modal's guide!
Persistence blooms, the guard stands tall, 🛡️

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main changes: quiz security encryption, session persistence, and UX fixes, which aligns with the substantial changeset across components, utilities, and database.
Linked Issues check ✅ Passed All coding requirements from linked issues are met: #115 (dashboard link fixed), #117 (QuizCard height standardized), #122 (category filter synced to URL), #124 (session persistence and quit modal implemented), #130 (encrypted answers with server verification).
Out of Scope Changes check ✅ Passed All changes are directly aligned with the five linked issues. Minor additions like database migrations for user_id foreign key and isIncomplete state support the core objectives and are necessary for functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
frontend/components/quiz/QuizResult.tsx (1)

90-119: Incomplete state says “result not counted” but still shows score/percentage bar.
If isIncomplete is meant to invalidate scoring, consider rendering “answeredCount / total” (and hiding the % bar) to avoid contradictory UI.

frontend/components/quiz/QuizQuestion.tsx (1)

10-18: Consider tri-state correctness (unknown vs false) and null-safe question text.

QuizQuestionClient.questionText can be null, and isCorrect is required even when answering—making “verification failed/unknown” indistinguishable from “incorrect”.

Proposed adjustment (optional)
 interface QuizQuestionProps {
   question: QuizQuestionClient;
   status: 'answering' | 'revealed';
   selectedAnswerId: string | null;
-  isCorrect: boolean;
+  isCorrect: boolean | null; // null = unknown (e.g., verify call failed)
   onAnswer: (answerId: string) => void;
   onNext: () => void;
   isLoading?: boolean;
 }

 export function QuizQuestion({
   question,
   status,
   selectedAnswerId,
   isCorrect,
   onAnswer,
   onNext,
   isLoading = false,
 }: QuizQuestionProps) {
   const t = useTranslations('quiz.question');
   const isAnswering = status === 'answering';
   const isRevealed = status === 'revealed';

-  const isCorrectAnswer = isRevealed && isCorrect;
+  const isCorrectAnswer = isRevealed && isCorrect === true;

   return (
     <div className="flex flex-col gap-6">
       <div className="text-xl font-medium text-gray-900 dark:text-gray-100">
-        {question.questionText}
+        {question.questionText ?? t('missingQuestionText')}
       </div>
frontend/components/quiz/QuizContainer.tsx (1)

60-75: Fix duplicate/out-of-order answer submissions (async verify can corrupt quiz state).

Minimal fix: disable during verify + make reducer idempotent per question
 export function QuizContainer({
   quizSlug,
   quizId,
   questions,
   encryptedAnswers,
   userId,
   timeLimitSeconds,
   seed,
   categorySlug,
   onBackToTopics,
 }: QuizContainerProps) {
@@
   const [showExitModal, setShowExitModal] = useState(false);
+  const [isVerifyingAnswer, setIsVerifyingAnswer] = useState(false);
@@
 function quizReducer(state: QuizState, action: QuizAction): QuizState {
   switch (action.type) {
@@
     case 'ANSWER_SELECTED':
+      // Ensure 1 answer per question (async verify can otherwise duplicate)
+      const answersWithoutThisQuestion = state.answers.filter(
+        a => a.questionId !== action.payload.questionId
+      );
       return {
         ...state,
         selectedAnswerId: action.payload.answerId,
         questionStatus: 'revealed',
         answers: [
-          ...state.answers,
+          ...answersWithoutThisQuestion,
           {
             questionId: action.payload.questionId,
             selectedAnswerId: action.payload.answerId,
             isCorrect: action.payload.isCorrect,
             answeredAt: new Date(),
           },
         ],
       };
@@
- const handleAnswer = async (answerId: string) => {
-  const response = await fetch('/api/quiz/verify-answer', {
-    method: 'POST',
-    headers: { 'Content-Type': 'application/json' },
-    body: JSON.stringify({
-      questionId: currentQuestion.id,
-      answerId,
-      encryptedAnswers,
-    }),
-  });
-  
-  const { isCorrect } = await response.json();
-
-  dispatch({
-    type: 'ANSWER_SELECTED',
-    payload: {
-      answerId,
-      isCorrect,
-      questionId: currentQuestion.id,
-    },
-  });
-};
+ const handleAnswer = async (answerId: string) => {
+   if (state.questionStatus !== 'answering') return;
+   if (isVerifyingAnswer) return;
+
+   setIsVerifyingAnswer(true);
+   try {
+     const response = await fetch('/api/quiz/verify-answer', {
+       method: 'POST',
+       headers: { 'Content-Type': 'application/json' },
+       body: JSON.stringify({
+         questionId: currentQuestion.id,
+         answerId,
+         encryptedAnswers,
+       }),
+     });
+
+     if (!response.ok) {
+       throw new Error(`verify-answer failed: ${response.status}`);
+     }
+
+     const data: unknown = await response.json();
+     const isCorrect = Boolean((data as any)?.isCorrect);
+
+     dispatch({
+       type: 'ANSWER_SELECTED',
+       payload: {
+         answerId,
+         isCorrect,
+         questionId: currentQuestion.id,
+       },
+     });
+   } catch (e) {
+     console.error(e);
+     // Option: surface a toast and keep questionStatus='answering' so user can retry
+   } finally {
+     setIsVerifyingAnswer(false);
+   }
+ };
@@
       <QuizQuestion
         question={currentQuestion}
         status={state.questionStatus}
         selectedAnswerId={state.selectedAnswerId}
         isCorrect={state.answers.find(a => a.questionId === currentQuestion.id)?.isCorrect ?? false}
         onAnswer={handleAnswer}
         onNext={handleNext}
-        isLoading={isPending}
+        isLoading={isPending || isVerifyingAnswer}
       />

Also applies to: 203-225, 436-444

frontend/app/[locale]/quiz/[slug]/page.tsx (1)

11-34: Guard against NaN seeds from invalid query parameters.

The seed parsing needs validation. If seedParam is non-numeric (e.g., "abc"), parseInt(seedParam, 10) returns NaN, which breaks the seeded randomization. In randomizeQuizQuestions, when seed is NaN, the seededRandom function returns NaN for all indices, making the sort comparisons undefined and shuffling unpredictable.

Safer seed parsing
-  const seed = seedParam ? parseInt(seedParam, 10) : Date.now();
+  const parsedSeed = seedParam ? Number(seedParam) : NaN;
+  const seed = Number.isFinite(parsedSeed) ? Math.trunc(parsedSeed) : Date.now();
🤖 Fix all issues with AI agents
In @frontend/app/api/quiz/verify-answer/route.ts:
- Around line 22-42: This endpoint leaks answer correctness and must enforce
authentication and rate limiting: call getCurrentUser() at the start of the
handler and return a 401/403 when it returns null, then apply a per-user
(fallback to IP) rate limit check before using decryptAnswers/enforcing logic
(use or add a rate limiter that returns 429 when exceeded); ensure the
rate-limit check occurs before returning the isCorrect response and include the
user id (or IP) when recording attempts so brute-force attempts are blocked for
that identity.
- Around line 10-48: In POST, treat JSON parse failures from request.json() as a
400: wrap the request.json() call in a try/catch (or detect SyntaxError) so
malformed JSON returns NextResponse.json({ error: 'Invalid JSON' }, { status:
400 }) and does not fall through to the generic 500 handler; keep the existing
field validation and decryptAnswers logic (decryptAnswers, correctAnswersMap,
correctAnswerId) unchanged, and add an explicit error log (e.g., console.error
or the project logger) in the generic catch block to record unexpected errors
before returning the 500 response.

In @frontend/components/quiz/QuizCard.tsx:
- Around line 55-72: Clamp the computed percentage used in the UI to the 0–100
range before rendering: ensure the value derived from userProgress.bestScore and
userProgress.totalQuestions (used as percentage) is sanitized (e.g., compute a
local percentage variable inside QuizCard component and set percentage =
Math.min(100, Math.max(0, rawPercentage))) and then use that clamped percentage
for the text display and the progress bar width. Update references to percentage
in the span and the inline style to use the clamped variable so widths never go
below 0% or above 100%.

In @frontend/components/ui/confirm-modal.tsx:
- Around line 34-64: Add minimal accessibility attributes and Escape-to-close
handling to the modal: give the modal container role="dialog" and
aria-modal="true", wire aria-labelledby and aria-describedby to IDs for the
title and message (create stable IDs via useId or a simple unique constant),
ensure the title element has that title ID and the message has the description
ID, and make the dialog container focusable (tabIndex={-1}) and focus it on
mount. Also add a keydown listener in a useEffect that calls onCancel when
Escape is pressed and cleans up the listener on unmount; keep existing overlay
onClick handler. Update references to onCancel, onConfirm, title, message,
cancelText, confirmText, and confirmButtonClass accordingly.

In @frontend/drizzle/0013_brown_gamora.sql:
- Around line 1-3: Migration 0015_dear_legion.sql contains a duplicate ALTER
TABLE that re-adds the foreign key constraint quiz_attempts_user_id_users_id_fk
which was already created in 0013_brown_gamora.sql; open 0015_dear_legion.sql
and remove the ALTER TABLE ... ADD CONSTRAINT
"quiz_attempts_user_id_users_id_fk" FOREIGN KEY (...) ... statement (or if you
must keep it, replace it with a DROP CONSTRAINT IF EXISTS
"quiz_attempts_user_id_users_id_fk" before the ADD), ensuring only one creation
of quiz_attempts_user_id_users_id_fk across migrations.

In @frontend/drizzle/0015_dear_legion.sql:
- Line 1: The migration 0015_dear_legion.sql is adding a duplicate foreign key
constraint "quiz_attempts_user_id_users_id_fk" which already exists in
0013_brown_gamora.sql; fix by removing the redundant migration file
(0015_dear_legion.sql) or make the ALTER TABLE idempotent (use an IF NOT EXISTS
variant or conditional check supported by your SQL dialect) or regenerate
migrations from a clean schema so only one migration creates
"quiz_attempts_user_id_users_id_fk".

In @frontend/hooks/useQuizGuards.ts:
- Around line 95-113: The popstate handler (handlePopState inside the useEffect)
currently calls window.history.pushState on each canceled back attempt which can
bloat history; change it to use window.history.replaceState or maintain a single
guard token so you only set one history entry per quiz session, and ensure you
don't repeatedly add entries when statusRef.current !== 'in_progress' or
isQuittingRef.current is true. Also replace the hardcoded confirm string with
your localized prompt (via next-intl or the app's ConfirmModal flow) so the
confirm text is consistent; keep the existing cleanup/logic around
clearQuizSession(quizId), resetViolations, and onExit unchanged.

In @frontend/hooks/useQuizSession.ts:
- Around line 59-75: Remove the redundant savedAt assignment from the
sessionData object construction in useQuizSession; the saveQuizSession function
already stamps savedAt, so delete the savedAt: Date.now() property when building
sessionData and continue to call saveQuizSession(quizId, sessionData) as before
to let saveQuizSession set the timestamp.
- Around line 56-75: The persisted session currently includes
answers[].isCorrect even though correctness is recomputed server-side; update
the effect in useQuizSession (the mapping that builds QuizSessionData before
calling saveQuizSession) to only persist questionId, selectedAnswerId and
answeredAt (remove isCorrect), adjust the constructed session object passed to
saveQuizSession and any related type/interface (QuizSessionData or the answers
item type) to reflect the reduced shape so runtime and TS types align.

In @frontend/lib/quiz/quiz-crypto.ts:
- Around line 1-13: Add a server-only marker at the top of quiz-crypto.ts (e.g.,
the established "server-only" directive used in the repo) and harden
getEncryptionKey(): ensure process.env.QUIZ_ENCRYPTION_KEY exists, validate it
matches exactly 64 hex chars (or /^[0-9a-fA-F]{64}$/) and/or that
Buffer.from(key,'hex').length === 32, and throw a clear error if not; ensure
encryptAnswers, decryptAnswers, and createEncryptedAnswersBlob call the
validated getEncryptionKey() so they never operate with an invalid key.

In @frontend/lib/quiz/quiz-storage-keys.ts:
- Around line 1-5: Replace the hardcoded 'quiz-allow-restore' string in the
LanguageSwitcher component with the exported constant QUIZ_ALLOW_RESTORE_KEY:
import QUIZ_ALLOW_RESTORE_KEY from the quiz-storage-keys module and use that
constant wherever the literal is used (specifically the value used at the place
corresponding to the previous literal in the LanguageSwitcher component),
removing the unused constant from being idle; ensure imports compile and update
any tests or usages that expect the literal string to now reference
QUIZ_ALLOW_RESTORE_KEY.

In @frontend/messages/en.json:
- Around line 154-159: The exitModal JSON object uses 2-space indentation
inconsistent with the rest of the file; update the indentation for the
"exitModal" block so nested lines (title, message, confirm, cancel) use 4 spaces
to match the file's style, ensuring the keys remain unchanged and the object
formatting aligns with the surrounding JSON entries.
🧹 Nitpick comments (7)
frontend/components/quiz/QuizzesSection.tsx (1)

52-56: Variable shadowing: params shadows outer scope variable.

On line 53, params shadows the params from useParams() declared on line 36. While this doesn't cause a bug since the inner variable is scoped to this function, it reduces readability and could confuse maintainers.

Suggested rename
  const handleCategoryChange = (category: string) => {
-   const params = new URLSearchParams(searchParams.toString());
-   params.set('category', category);
-   router.replace(`?${params.toString()}`, { scroll: false });
+   const urlParams = new URLSearchParams(searchParams.toString());
+   urlParams.set('category', category);
+   router.replace(`?${urlParams.toString()}`, { scroll: false });
  };
frontend/components/quiz/PendingResultHandler.tsx (1)

12-15: Redundant conditional check.

The if (pending) check on line 15 is unnecessary since the early return on line 13 already handles the case when pending is falsy.

♻️ Suggested simplification
   useEffect(() => {
     const pending = getPendingQuizResult();
     if (!pending) return;
 
-    if (pending) {
-      fetch("/api/quiz/guest-result", {
+    fetch("/api/quiz/guest-result", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify({
         userId,
         quizId: pending.quizId,
         answers: pending.answers,
         violations: pending.violations,
         timeSpentSeconds: pending.timeSpentSeconds,
       }),
     })
       .then(async (res) => {
         if (res.ok) {
           clearPendingQuizResult();
         } else {
           console.error("Guest-result API error:", res.status);
         }
       })
       .catch(err => {
         console.error("Guest-result fetch error:", err);
       });
-    }
   }, [userId]);
frontend/messages/pl.json (1)

154-159: JSON formatting inconsistency.

The exitModal block has inconsistent indentation compared to its sibling keys (like section, result, etc.). For maintainability and consistency with the rest of the file, adjust the indentation to match.

♻️ Suggested formatting fix
     "section": {
       "noQuizzes": "W tej kategorii nie ma jeszcze quizów."
     },
     "exitModal": {
-  "title": "Wyjść z quizu?",
-  "message": "Twój postęp nie zostanie zapisany.",
-  "confirm": "Wyjdź",
-  "cancel": "Kontynuuj"
-}
+      "title": "Wyjść z quizu?",
+      "message": "Twój postęp nie zostanie zapisany.",
+      "confirm": "Wyjdź",
+      "cancel": "Kontynuuj"
+    }
   },
frontend/components/shared/LanguageSwitcher.tsx (2)

4-4: Minor formatting: missing space after comma.

♻️ Suggested fix
-import { useParams, usePathname,useSearchParams } from 'next/navigation';
+import { useParams, usePathname, useSearchParams } from 'next/navigation';

18-19: Import and use the QUIZ_ALLOW_RESTORE_KEY constant from frontend/lib/quiz/quiz-storage-keys.ts.

The constant is defined in the shared storage keys file and currently duplicated as a local string. Replace the hardcoded 'quiz-allow-restore' with the imported constant to ensure consistency across the codebase and prevent key mismatches if the value changes.

frontend/components/quiz/QuizCard.tsx (1)

55-66: Prefer next-intl pluralization over manual singular/plural branching.
Line 61 hardcodes English plural rules; this can break for locales with multiple plural forms.

Proposed direction
- {userProgress.attemptsCount} {userProgress.attemptsCount === 1 ? t('attempt') : t('attempts')}
+ {t('attempts', { count: userProgress.attemptsCount })}
frontend/hooks/useQuizSession.ts (1)

35-55: Avoid re-running restore effect due to unstable onRestore.
Because Line 54 depends on onRestore, a non-memoized callback can cause re-restore / unexpected clearQuizSession() on re-renders.

Suggested adjustment
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';

 export function useQuizSession({ quizId, state, onRestore }: UseQuizSessionParams): void {
   const reloadKey = getQuizReloadKey(quizId);
+  const onRestoreRef = useRef(onRestore);
+  onRestoreRef.current = onRestore;

   useEffect(() => {
@@
     if (isReload || allowRestore) {
-      onRestore(saved);
+      onRestoreRef.current(saved);
     } else {
       clearQuizSession(quizId);
     }
-  }, [quizId, reloadKey, onRestore]);
+  }, [quizId, reloadKey]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 777219f and 1b62dd4.

📒 Files selected for processing (29)
  • frontend/app/[locale]/login/page.tsx
  • frontend/app/[locale]/quiz/[slug]/page.tsx
  • frontend/app/[locale]/signup/page.tsx
  • frontend/app/api/quiz/verify-answer/route.ts
  • frontend/components/dashboard/StatsCard.tsx
  • frontend/components/quiz/PendingResultHandler.tsx
  • frontend/components/quiz/QuizCard.tsx
  • frontend/components/quiz/QuizContainer.tsx
  • frontend/components/quiz/QuizQuestion.tsx
  • frontend/components/quiz/QuizResult.tsx
  • frontend/components/quiz/QuizzesSection.tsx
  • frontend/components/shared/LanguageSwitcher.tsx
  • frontend/components/ui/confirm-modal.tsx
  • frontend/db/queries/quiz.ts
  • frontend/db/schema/quiz.ts
  • frontend/drizzle/0013_brown_gamora.sql
  • frontend/drizzle/0015_dear_legion.sql
  • frontend/drizzle/meta/0013_snapshot.json
  • frontend/drizzle/meta/0015_snapshot.json
  • frontend/drizzle/meta/_journal.json
  • frontend/hooks/useQuizGuards.ts
  • frontend/hooks/useQuizSession.ts
  • frontend/lib/quiz/guest-quiz.ts
  • frontend/lib/quiz/quiz-crypto.ts
  • frontend/lib/quiz/quiz-session.ts
  • frontend/lib/quiz/quiz-storage-keys.ts
  • frontend/messages/en.json
  • frontend/messages/pl.json
  • frontend/messages/uk.json
🧰 Additional context used
🧬 Code graph analysis (10)
frontend/db/queries/quiz.ts (1)
frontend/db/schema/questions.ts (1)
  • questions (15-37)
frontend/app/api/quiz/verify-answer/route.ts (1)
frontend/lib/quiz/quiz-crypto.ts (1)
  • decryptAnswers (42-64)
frontend/hooks/useQuizGuards.ts (2)
frontend/lib/quiz/quiz-storage-keys.ts (1)
  • getQuizReloadKey (3-5)
frontend/lib/quiz/quiz-session.ts (1)
  • clearQuizSession (58-66)
frontend/components/quiz/QuizzesSection.tsx (1)
frontend/data/category.ts (1)
  • categoryData (11-22)
frontend/app/[locale]/quiz/[slug]/page.tsx (2)
frontend/db/queries/quiz.ts (2)
  • getQuizQuestionsRandomized (284-291)
  • stripCorrectAnswers (63-78)
frontend/lib/quiz/quiz-crypto.ts (1)
  • createEncryptedAnswersBlob (69-82)
frontend/components/quiz/PendingResultHandler.tsx (2)
frontend/scripts/shop-janitor-restock-stale.mjs (1)
  • res (54-64)
frontend/lib/quiz/guest-quiz.ts (1)
  • clearPendingQuizResult (51-54)
frontend/hooks/useQuizSession.ts (2)
frontend/lib/quiz/quiz-session.ts (4)
  • QuizSessionData (4-17)
  • loadQuizSession (30-56)
  • clearQuizSession (58-66)
  • saveQuizSession (19-28)
frontend/lib/quiz/quiz-storage-keys.ts (2)
  • getQuizReloadKey (3-5)
  • QUIZ_ALLOW_RESTORE_KEY (1-1)
frontend/components/ui/confirm-modal.tsx (1)
frontend/components/ui/button.tsx (1)
  • Button (42-42)
frontend/components/quiz/QuizQuestion.tsx (1)
frontend/db/queries/quiz.ts (1)
  • QuizQuestionClient (54-61)
frontend/db/schema/quiz.ts (1)
frontend/db/schema/users.ts (1)
  • users (11-42)
🔇 Additional comments (27)
frontend/drizzle/meta/_journal.json (1)

95-102: LGTM!

The new migration entry (idx 13, tag 0013_brown_gamora) follows the correct format, maintains sequential indexing, and has a chronologically valid timestamp. This auto-generated journal entry properly tracks the new database migration for the quiz-related schema changes.

frontend/components/dashboard/StatsCard.tsx (2)

55-61: LGTM!

The "Start a Quiz" button now correctly links to /quizzes, which aligns with the broader quiz flow routing updates in this PR.


84-86: No action needed. The route /q&a is correct and fully implemented in the application. The directory structure at frontend/app/[locale]/q&a/ confirms this is the intended route, and it's consistently referenced throughout the codebase including in frontend/lib/navigation.ts. While ampersand characters are unconventional in URLs, they are valid in path segments and do not cause parsing issues in this context.

frontend/components/quiz/QuizzesSection.tsx (3)

46-50: LGTM!

The URL-driven category state is well-implemented:

  • Safe fallback with categoryData[0]?.slug || 'git'
  • Proper validation against known categories before using URL value
  • Non-null assertion on line 50 is safe since validCategory guarantees categoryFromUrl is not null when true

This correctly addresses issue #122 by preserving the category selection in URL params across language switches.


61-61: LGTM!

The controlled Tabs component correctly reflects and updates the URL-based category state.


37-38: Consider Suspense boundary for useSearchParams.

In Next.js App Router, useSearchParams() must be inside a <Suspense> boundary when the route can be statically prerendered. Without it, the component tree will be forced to client-side rendering or the build will fail. Wrap this component (or a parent) in <Suspense fallback={...}>, or alternatively, accept searchParams in a Server Component and pass them as props, or mark the route as dynamic.

frontend/db/schema/quiz.ts (2)

133-135: LGTM! Foreign key constraint properly defined.

The onDelete: 'cascade' ensures referential integrity—quiz attempts are automatically cleaned up when a user is deleted. The column type (text) correctly matches users.id.


154-155: LGTM! Indexes support common query patterns.

The standalone userIdIdx is useful for queries that filter only by userId (e.g., "get all attempts for a user"). While userCompletedAtIdx also covers userId, it's optimized for ordered lookups including completedAt. Both serve distinct query patterns.

frontend/drizzle/meta/0013_snapshot.json (3)

576-605: LGTM! Snapshot correctly captures the new indexes.

The index definitions for quiz_attempts_user_id_idx and quiz_attempts_quiz_id_idx are consistent with the schema in quiz.ts and the migration in 0013_brown_gamora.sql.


677-689: LGTM! Foreign key definition is consistent.

The FK quiz_attempts_user_id_users_id_fk with onDelete: cascade correctly reflects the schema changes.


1225-1262: Unrelated schema addition: internal_job_state table.

This table appears unrelated to the quiz flow objectives in this PR. Consider whether this change should be in a separate PR for clearer change tracking, or confirm it's intentionally bundled.

frontend/messages/uk.json (1)

153-159: LGTM!

The Ukrainian translations for the exit modal are well-structured and consistent with the file's indentation style. The translations appropriately convey the exit confirmation messaging.

frontend/lib/quiz/quiz-session.ts (4)

19-28: LGTM!

The saveQuizSession function correctly handles SSR environments with an early return, uses try-catch for localStorage errors (which can throw in private browsing mode), and properly timestamps the session.


30-56: LGTM!

The loadQuizSession function implements proper validation:

  • SSR guard for server-side rendering
  • TTL expiration check (30 minutes)
  • Status validation to only restore in_progress sessions
  • Automatic cleanup of stale/invalid sessions
  • Error handling for parse failures

58-66: LGTM!

The clearQuizSession function correctly handles SSR environments and localStorage errors.


7-11: Remove this comment — the security concern is not applicable.

The isCorrect field stored in localStorage (line 10) is the result of server-side verification from the /api/quiz/verify-answer endpoint, not the answer key itself. The correct answer mapping is encrypted server-side and never exposed to the client. Storing the verification result in localStorage is appropriate for session persistence and does not undermine security. Answer correctness is only displayed in the UI when questionStatus === 'revealed', preventing exposure during the answering phase.

frontend/app/[locale]/signup/page.tsx (1)

7-7: LGTM!

The import path update aligns with the PR's reorganization of quiz utilities under lib/quiz/. This is consistent with similar changes in other files (login page, PendingResultHandler).

frontend/app/[locale]/login/page.tsx (1)

7-7: LGTM!

Import path correctly updated to reflect the new module organization under lib/quiz/.

frontend/components/quiz/PendingResultHandler.tsx (1)

4-4: LGTM!

Import path correctly updated to align with the new module structure.

frontend/db/queries/quiz.ts (1)

48-78: LGTM!

Well-designed client-safe types and utility function. The QuizAnswerClient and QuizQuestionClient interfaces correctly omit the isCorrect field to prevent client-side cheating, and stripCorrectAnswers provides a clean transformation from server-side to client-safe data structures.

frontend/components/shared/LanguageSwitcher.tsx (1)

65-72: LGTM!

Query parameter preservation correctly addresses issue #122 (language switch resetting category filter). The sessionStorage flag for quiz page restoration integrates well with the broader quiz session management flow.

frontend/components/quiz/QuizCard.tsx (1)

31-53: Flex + line-clamp change achieves consistent card heights (nice).
This structure should help keep the CTA aligned across the grid.

frontend/app/api/quiz/verify-answer/route.ts (1)

1-3: Ensure this route runs in the Node.js runtime (not Edge).
decryptAnswers() uses Node crypto/Buffer patterns; verify no runtime = 'edge' is set (here or globally) for this handler.

frontend/components/ui/confirm-modal.tsx (1)

28-32: Confirm button styling: verify it doesn’t fight Button’s variants.
Since you’re passing raw Tailwind classes (Line 57), confirm Button doesn’t enforce conflicting background/text classes that override these.

Also applies to: 55-60

frontend/components/quiz/QuizResult.tsx (1)

16-45: /

frontend/components/quiz/QuizQuestion.tsx (1)

46-76: Correct/incorrect labeling logic looks consistent with server-verified flow.

frontend/app/[locale]/quiz/[slug]/page.tsx (1)

32-34: Passing encryptedAnswers to the client is OK, but ensure verify endpoint can’t be used as a “correctness oracle”.

Even with encryption, a user can still brute-force by calling /api/quiz/verify-answer for each option unless the server ties verification to an attempt/session and rate-limits (or only allows one verification per question). Please sanity-check the API route behavior.

Also applies to: 63-72

Comment on lines +10 to +48
export async function POST(request: NextRequest) {
try {
const body: VerifyRequest = await request.json();
const { questionId, answerId, encryptedAnswers } = body;

if (!questionId || !answerId || !encryptedAnswers) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}

const correctAnswersMap = decryptAnswers(encryptedAnswers);

if (!correctAnswersMap) {
return NextResponse.json(
{ error: 'Invalid encrypted data' },
{ status: 400 }
);
}

const correctAnswerId = correctAnswersMap[questionId];

if (!correctAnswerId) {
return NextResponse.json(
{ error: 'Question not found' },
{ status: 404 }
);
}

return NextResponse.json({
isCorrect: answerId === correctAnswerId,
});
} catch {
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and read the file
find . -name "verify-answer" -o -name "route.ts" | grep verify-answer

Repository: DevLoversTeam/devlovers.net

Length of output: 155


🏁 Script executed:

# List files in the quiz API directory
fd route.ts frontend/app/api/quiz/

Repository: DevLoversTeam/devlovers.net

Length of output: 197


🏁 Script executed:

# List the structure of the frontend/app/api directory
find frontend/app/api -type f -name "route.ts" 2>/dev/null | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 971


🏁 Script executed:

# Read the verify-answer route file
cat -n frontend/app/api/quiz/verify-answer/route.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 1595


🏁 Script executed:

# Check how other API routes in the codebase handle request.json() errors
rg -A 5 "request\.json\(\)" frontend/app/api --type ts

Repository: DevLoversTeam/devlovers.net

Length of output: 1601


🏁 Script executed:

# Check for error handling patterns in other routes
rg -A 10 "catch\s*\(" frontend/app/api --type ts -m 3

Repository: DevLoversTeam/devlovers.net

Length of output: 21134


🏁 Script executed:

# Verify error handling in quiz/guest-result for comparison
cat -n frontend/app/api/quiz/guest-result/route.ts | head -35

Repository: DevLoversTeam/devlovers.net

Length of output: 1371


🏁 Script executed:

# Check if there's a logError utility in the codebase
rg "logError" frontend --type ts | head -5

Repository: DevLoversTeam/devlovers.net

Length of output: 529


Return 400 (not 500) for invalid JSON; log unexpected errors.

request.json() throws on malformed JSON, which is a client error, not a server error. The codebase consistently returns 400 for JSON parsing failures (see quiz/guest-result, auth/signup, auth/login). Also add explicit error logging to distinguish between client-side JSON errors and genuine server failures.

Proposed fix
 export async function POST(request: NextRequest) {
   try {
-    const body: VerifyRequest = await request.json();
+    let body: VerifyRequest;
+    try {
+      body = await request.json();
+    } catch {
+      return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
+    }
     const { questionId, answerId, encryptedAnswers } = body;
@@
-  } catch {
+  } catch (e) {
+    console.error('verify-answer failed', e);
     return NextResponse.json(
       { error: 'Internal server error' },
       { status: 500 }
     );
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function POST(request: NextRequest) {
try {
const body: VerifyRequest = await request.json();
const { questionId, answerId, encryptedAnswers } = body;
if (!questionId || !answerId || !encryptedAnswers) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
const correctAnswersMap = decryptAnswers(encryptedAnswers);
if (!correctAnswersMap) {
return NextResponse.json(
{ error: 'Invalid encrypted data' },
{ status: 400 }
);
}
const correctAnswerId = correctAnswersMap[questionId];
if (!correctAnswerId) {
return NextResponse.json(
{ error: 'Question not found' },
{ status: 404 }
);
}
return NextResponse.json({
isCorrect: answerId === correctAnswerId,
});
} catch {
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
export async function POST(request: NextRequest) {
try {
let body: VerifyRequest;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}
const { questionId, answerId, encryptedAnswers } = body;
if (!questionId || !answerId || !encryptedAnswers) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
const correctAnswersMap = decryptAnswers(encryptedAnswers);
if (!correctAnswersMap) {
return NextResponse.json(
{ error: 'Invalid encrypted data' },
{ status: 400 }
);
}
const correctAnswerId = correctAnswersMap[questionId];
if (!correctAnswerId) {
return NextResponse.json(
{ error: 'Question not found' },
{ status: 404 }
);
}
return NextResponse.json({
isCorrect: answerId === correctAnswerId,
});
} catch (e) {
console.error('verify-answer failed', e);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
🤖 Prompt for AI Agents
In @frontend/app/api/quiz/verify-answer/route.ts around lines 10 - 48, In POST,
treat JSON parse failures from request.json() as a 400: wrap the request.json()
call in a try/catch (or detect SyntaxError) so malformed JSON returns
NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }) and does not fall
through to the generic 500 handler; keep the existing field validation and
decryptAnswers logic (decryptAnswers, correctAnswersMap, correctAnswerId)
unchanged, and add an explicit error log (e.g., console.error or the project
logger) in the generic catch block to record unexpected errors before returning
the 500 response.

Comment on lines +22 to +42
const correctAnswersMap = decryptAnswers(encryptedAnswers);

if (!correctAnswersMap) {
return NextResponse.json(
{ error: 'Invalid encrypted data' },
{ status: 400 }
);
}

const correctAnswerId = correctAnswersMap[questionId];

if (!correctAnswerId) {
return NextResponse.json(
{ error: 'Question not found' },
{ status: 404 }
);
}

return NextResponse.json({
isCorrect: answerId === correctAnswerId,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n frontend/app/api/quiz/verify-answer/route.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 1595


🏁 Script executed:

fd -name "middleware.ts" -o -name "middleware.js"

Repository: DevLoversTeam/devlovers.net

Length of output: 301


🏁 Script executed:

rg -l "rate" --type ts --type tsx frontend/app/api/ | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

rg "NextRequest" frontend/app/api/ -A 3 | grep -E "(auth|middleware)" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 1637


🏁 Script executed:

find . -name "middleware.ts" -o -name "middleware.js" | head -10

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

rg "rateLimit|Ratelimit|throttle" --type ts | head -30

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

rg "quiz/verify-answer" -A 5 -B 5

Repository: DevLoversTeam/devlovers.net

Length of output: 908


🏁 Script executed:

rg "getCurrentUser|auth" frontend/app/api/quiz/ --type ts

Repository: DevLoversTeam/devlovers.net

Length of output: 338


🏁 Script executed:

cat -n frontend/lib/auth.ts 2>/dev/null | head -50

Repository: DevLoversTeam/devlovers.net

Length of output: 1517


Add authentication and rate limiting to prevent brute-force attempts.

This endpoint accepts unauthenticated requests and directly returns whether an answerId is correct (Line 41). A client can iterate through all possible answer IDs to determine correct answers without any constraints. The codebase has an auth system (getCurrentUser() in /lib/auth.ts) that's used in other quiz endpoints like guest-result, but it's missing here. Implement authentication (via getCurrentUser()) and add per-user/per-attempt rate limiting or constraints to prevent answer enumeration.

🤖 Prompt for AI Agents
In @frontend/app/api/quiz/verify-answer/route.ts around lines 22 - 42, This
endpoint leaks answer correctness and must enforce authentication and rate
limiting: call getCurrentUser() at the start of the handler and return a 401/403
when it returns null, then apply a per-user (fallback to IP) rate limit check
before using decryptAnswers/enforcing logic (use or add a rate limiter that
returns 429 when exceeded); ensure the rate-limit check occurs before returning
the isCorrect response and include the user id (or IP) when recording attempts
so brute-force attempts are blocked for that identity.

Comment on lines +55 to +72
<div className="mb-6">
<div className="flex justify-between text-xs mb-1.5">
<span className="text-gray-600 dark:text-gray-400">
{t('best')} {userProgress.bestScore}/{userProgress.totalQuestions}
</span>
<span className="text-gray-500">
{userProgress.attemptsCount} {userProgress.attemptsCount === 1 ? t('attempt') : t('attempts')}
</span>
<span className="font-medium text-gray-900 dark:text-gray-100">
{percentage}%
</span>
</div>
<div className="h-1.5 bg-gray-200 dark:bg-neutral-800 rounded-full overflow-hidden">
<div
className="h-full bg-blue-600 rounded-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp percentage to [0, 100] before using it in UI.
If bestScore/totalQuestions is ever out of sync, Line 70 could render widths >100% (and negative widths if data is bad).

Proposed fix
-  const percentage = userProgress && userProgress.totalQuestions > 0
-    ? Math.round((userProgress.bestScore / userProgress.totalQuestions) * 100)
-    : 0;
+  const rawPercentage =
+    userProgress && userProgress.totalQuestions > 0
+      ? Math.round((userProgress.bestScore / userProgress.totalQuestions) * 100)
+      : 0;
+  const percentage = Math.min(100, Math.max(0, rawPercentage));
🤖 Prompt for AI Agents
In @frontend/components/quiz/QuizCard.tsx around lines 55 - 72, Clamp the
computed percentage used in the UI to the 0–100 range before rendering: ensure
the value derived from userProgress.bestScore and userProgress.totalQuestions
(used as percentage) is sanitized (e.g., compute a local percentage variable
inside QuizCard component and set percentage = Math.min(100, Math.max(0,
rawPercentage))) and then use that clamped percentage for the text display and
the progress bar width. Update references to percentage in the span and the
inline style to use the clamped variable so widths never go below 0% or above
100%.

Comment on lines +303 to +309
const confirmQuit = () => {
markQuitting();
clearQuizSession(quizId);
resetViolations();
const categoryParam = categorySlug ? `?category=${categorySlug}` : '';
window.location.href = `/${locale}/quizzes${categoryParam}`;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

URL-build for quit should encode categorySlug (and prefer router navigation if possible).

Safer query param
-  const categoryParam = categorySlug ? `?category=${categorySlug}` : '';
+  const categoryParam = categorySlug ? `?category=${encodeURIComponent(categorySlug)}` : '';

Comment on lines +34 to +64
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/50"
onClick={onCancel}
/>
<div className="relative bg-white dark:bg-neutral-900 rounded-xl p-6 max-w-sm mx-4 shadow-xl border border-gray-200 dark:border-neutral-800">
<h3 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-2">
{title}
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-6">
{message}
</p>
<div className="flex gap-3">
<Button
variant="outline"
onClick={onCancel}
className="flex-1"
>
{cancelText}
</Button>
<Button
onClick={onConfirm}
className={`flex-1 ${confirmButtonClass}`}
>
{confirmText}
</Button>
</div>
</div>
</div>
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add minimal modal accessibility + Escape-to-close.
This currently lacks role="dialog", aria-modal, label/description wiring, and keyboard handling (Esc).

Proposed fix
 import { Button } from '@/components/ui/button';
+import { useEffect, useId } from 'react';
@@
 export function ConfirmModal({
@@
 }: ConfirmModalProps) {
   if (!isOpen) return null;
+  const titleId = useId();
+  const messageId = useId();
+
+  useEffect(() => {
+    const onKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') onCancel();
+    };
+    window.addEventListener('keydown', onKeyDown);
+    return () => window.removeEventListener('keydown', onKeyDown);
+  }, [onCancel]);
@@
   return (
-    <div className="fixed inset-0 z-50 flex items-center justify-center">
+    <div
+      className="fixed inset-0 z-50 flex items-center justify-center"
+      role="dialog"
+      aria-modal="true"
+      aria-labelledby={titleId}
+      aria-describedby={messageId}
+    >
       <div 
         className="absolute inset-0 bg-black/50" 
         onClick={onCancel}
       />
       <div className="relative bg-white dark:bg-neutral-900 rounded-xl p-6 max-w-sm mx-4 shadow-xl border border-gray-200 dark:border-neutral-800">
-        <h3 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-2">
+        <h3 id={titleId} className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-2">
           {title}
         </h3>
-        <p className="text-gray-600 dark:text-gray-400 mb-6">
+        <p id={messageId} className="text-gray-600 dark:text-gray-400 mb-6">
           {message}
         </p>
🤖 Prompt for AI Agents
In @frontend/components/ui/confirm-modal.tsx around lines 34 - 64, Add minimal
accessibility attributes and Escape-to-close handling to the modal: give the
modal container role="dialog" and aria-modal="true", wire aria-labelledby and
aria-describedby to IDs for the title and message (create stable IDs via useId
or a simple unique constant), ensure the title element has that title ID and the
message has the description ID, and make the dialog container focusable
(tabIndex={-1}) and focus it on mount. Also add a keydown listener in a
useEffect that calls onCancel when Escape is pressed and cleans up the listener
on unmount; keep existing overlay onClick handler. Update references to
onCancel, onConfirm, title, message, cancelText, confirmText, and
confirmButtonClass accordingly.

Comment on lines +56 to +75
useEffect(() => {
if (state.status !== 'in_progress') return;

const sessionData: QuizSessionData = {
status: state.status,
currentIndex: state.currentIndex,
answers: state.answers.map(a => ({
questionId: a.questionId,
selectedAnswerId: a.selectedAnswerId,
isCorrect: a.isCorrect,
answeredAt: a.answeredAt.getTime(),
})),
questionStatus: state.questionStatus,
selectedAnswerId: state.selectedAnswerId,
startedAt: state.startedAt?.getTime() ?? null,
savedAt: Date.now(),
};

saveQuizSession(quizId, sessionData);
}, [quizId, state]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "useQuizSession.ts" -o -name "*QuizSession*" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 105


🏁 Script executed:

fd "useQuizSession" --type f

Repository: DevLoversTeam/devlovers.net

Length of output: 103


🏁 Script executed:

git ls-files | grep -i quiz

Repository: DevLoversTeam/devlovers.net

Length of output: 3671


🏁 Script executed:

cat -n frontend/hooks/useQuizSession.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 2603


🏁 Script executed:

cat -n frontend/lib/quiz/quiz-session.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 2284


🏁 Script executed:

cat -n frontend/app/\[locale\]/quiz/\[slug\]/page.tsx | head -150

Repository: DevLoversTeam/devlovers.net

Length of output: 3275


🏁 Script executed:

cat -n frontend/actions/quiz.ts | head -150

Repository: DevLoversTeam/devlovers.net

Length of output: 5200


🏁 Script executed:

cat -n frontend/app/api/quiz/verify-answer/route.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 1595


🏁 Script executed:

cat -n frontend/actions/quiz.ts | tail -100

Repository: DevLoversTeam/devlovers.net

Length of output: 3401


🏁 Script executed:

cat -n frontend/components/quiz/QuizContainer.tsx | head -200

Repository: DevLoversTeam/devlovers.net

Length of output: 7038


🏁 Script executed:

rg "onRestore" frontend/components/quiz/ -A 5

Repository: DevLoversTeam/devlovers.net

Length of output: 483


🏁 Script executed:

rg "answers.*isCorrect" frontend/components/quiz/ -B 2 -A 2

Repository: DevLoversTeam/devlovers.net

Length of output: 1400


🏁 Script executed:

cat -n frontend/components/quiz/QuizQuestion.tsx | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 4258


🏁 Script executed:

cat -n frontend/components/quiz/QuizContainer.tsx | sed -n '200,350p'

Repository: DevLoversTeam/devlovers.net

Length of output: 5349


🏁 Script executed:

rg "correctAnswers.*filter" frontend/components/quiz/ -B 3 -A 3

Repository: DevLoversTeam/devlovers.net

Length of output: 1191


🏁 Script executed:

cat -n frontend/lib/quiz/guest-quiz.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 1726


🏁 Script executed:

rg "savePendingQuizResult\|pendingQuizResult" frontend/ -B 2 -A 5

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

rg "getPendingQuizResult" frontend/ -B 2 -A 10

Repository: DevLoversTeam/devlovers.net

Length of output: 6505


🏁 Script executed:

rg "pendingQuizResult\|PendingQuizResult" frontend/components/ frontend/app/api/ -B 2 -A 5

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

cat -n frontend/app/api/quiz/guest-result/route.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 6631


🏁 Script executed:

cat -n frontend/components/quiz/PendingResultHandler.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 1394


🏁 Script executed:

rg "state.answers.*map" frontend/components/quiz/QuizContainer.tsx -B 2 -A 2

Repository: DevLoversTeam/devlovers.net

Length of output: 231


🏁 Script executed:

cat -n frontend/components/quiz/QuizContainer.tsx | sed -n '251,270p'

Repository: DevLoversTeam/devlovers.net

Length of output: 799


🏁 Script executed:

rg "submitQuizAttempt.*answers" frontend/components/quiz/QuizContainer.tsx -B 2 -A 8

Repository: DevLoversTeam/devlovers.net

Length of output: 53


Remove answers[].isCorrect from session persistence; server already recomputes correctness.

The persisted isCorrect values are unnecessary since both submitQuizAttempt() (actions/quiz.ts line 162) and the guest-result endpoint (guest-result/route.ts line 117) ignore client values and recalculate correctness directly from the database. Persist only {questionId, selectedAnswerId, answeredAt} to avoid misleading UI displays and reduce persistence overhead.

🤖 Prompt for AI Agents
In @frontend/hooks/useQuizSession.ts around lines 56 - 75, The persisted session
currently includes answers[].isCorrect even though correctness is recomputed
server-side; update the effect in useQuizSession (the mapping that builds
QuizSessionData before calling saveQuizSession) to only persist questionId,
selectedAnswerId and answeredAt (remove isCorrect), adjust the constructed
session object passed to saveQuizSession and any related type/interface
(QuizSessionData or the answers item type) to reflect the reduced shape so
runtime and TS types align.

Comment on lines +59 to +75
const sessionData: QuizSessionData = {
status: state.status,
currentIndex: state.currentIndex,
answers: state.answers.map(a => ({
questionId: a.questionId,
selectedAnswerId: a.selectedAnswerId,
isCorrect: a.isCorrect,
answeredAt: a.answeredAt.getTime(),
})),
questionStatus: state.questionStatus,
selectedAnswerId: state.selectedAnswerId,
startedAt: state.startedAt?.getTime() ?? null,
savedAt: Date.now(),
};

saveQuizSession(quizId, sessionData);
}, [quizId, state]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove redundant savedAt assignment (lib already stamps it).
Line 71 is overwritten inside saveQuizSession() (per frontend/lib/quiz/quiz-session.ts). Keeping only one source reduces confusion.

Proposed fix
-    const sessionData: QuizSessionData = {
+    const sessionData: QuizSessionData = {
       status: state.status,
@@
       selectedAnswerId: state.selectedAnswerId,
       startedAt: state.startedAt?.getTime() ?? null,
-      savedAt: Date.now(),
+      savedAt: 0,
     };
🤖 Prompt for AI Agents
In @frontend/hooks/useQuizSession.ts around lines 59 - 75, Remove the redundant
savedAt assignment from the sessionData object construction in useQuizSession;
the saveQuizSession function already stamps savedAt, so delete the savedAt:
Date.now() property when building sessionData and continue to call
saveQuizSession(quizId, sessionData) as before to let saveQuizSession set the
timestamp.

Comment on lines +1 to +13
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;

function getEncryptionKey(): Buffer {
const key = process.env.QUIZ_ENCRYPTION_KEY;
if (!key) {
throw new Error('QUIZ_ENCRYPTION_KEY environment variable is not set');
}
return Buffer.from(key, 'hex');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check the Next.js version in package.json
find . -name "package.json" -type f | head -5 | xargs cat | grep -A 10 -B 2 "next"

Repository: DevLoversTeam/devlovers.net

Length of output: 1597


🏁 Script executed:

# Read the full quiz-crypto.ts file to see lines 22-37
cat -n frontend/lib/quiz/quiz-crypto.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 2907


🏁 Script executed:

# Check if server-only is already in dependencies
find . -name "package.json" -type f | xargs grep -l "server-only" 2>/dev/null

Repository: DevLoversTeam/devlovers.net

Length of output: 55


🏁 Script executed:

# Search for existing server-only imports in the codebase
rg "import.*server-only" --type ts --type tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

# Search for server-only imports with correct rg syntax
rg "server-only" --type ts

Repository: DevLoversTeam/devlovers.net

Length of output: 665


🏁 Script executed:

# Check if server-only is a standard Next.js directive (built-in, no package needed)
web_search
"server-only" Next.js directive built-in package

Repository: DevLoversTeam/devlovers.net

Length of output: 171


🏁 Script executed:

# Search for where quiz-crypto.ts is imported
rg "quiz-crypto" --type ts --type tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

# Check if the file is used in client or server context
rg "encryptAnswers\|decryptAnswers" --type ts --type tsx -B 2 -A 2

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

# Search for server-only imports correctly
rg "server-only" -t ts

Repository: DevLoversTeam/devlovers.net

Length of output: 665


🏁 Script executed:

# Search for where quiz-crypto is imported
rg "quiz-crypto"

Repository: DevLoversTeam/devlovers.net

Length of output: 284


🏁 Script executed:

# Search for usage of the encryption functions
rg "encryptAnswers|decryptAnswers" -A 2 -B 2

Repository: DevLoversTeam/devlovers.net

Length of output: 1653


🌐 Web query:

Next.js server-only directive built-in feature

💡 Result:

Short answer

  • Next.js (via React Server Components) provides a built-in "use server" directive that marks a function or whole module as server-only (Server Function / Server Action). Use it inline at the top of an async function or at the top of a file to make all exports server-side. [1][2]

Key details

  • Two placements: module-level ('use server' at top of file) or function-level ('use server' at top of a function). Module-level actions can be imported into Client Components; inline actions work inside Server Components. [1][2]
  • Typical uses: server mutations, DB access, form handlers — Next.js will execute the code only on the server and (for client-invoked actions) expose an auto-generated server endpoint. [2]
  • Security: treat action inputs as untrusted; Next.js enforces POST-only invocation and host/origin checks (additional protections described in Next.js docs). [3]

Official docs / reading

  • "use server" directive reference. [1]
  • Server Actions / Server Functions overview and usage. [2]
  • Security considerations for Server Actions. [3]

Sources
[1] Next.js — Directives: use server.
[2] Next.js — Server Actions and Mutations / Functions: Server Actions.
[3] Next.js — Security guidance for Server Components & Actions.


🏁 Script executed:

# Check the quiz page component to see if it's a Server or Client Component
head -20 frontend/app/[locale]/quiz/[slug]/page.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 931


🏁 Script executed:

# Verify the package.json doesn't have server-only as a dependency (since it should be built-in)
cat frontend/package.json | grep -A 20 '"dependencies"'

Repository: DevLoversTeam/devlovers.net

Length of output: 768


🏁 Script executed:

# Check crypto module documentation - verify AES-256-GCM key requirements
web_search
AES-256-GCM cipher key length bytes requirements Node.js crypto

Repository: DevLoversTeam/devlovers.net

Length of output: 171


🌐 Web query:

Node.js crypto AES-256-GCM key length 32 bytes requirements

💡 Result:

Short answer: aes-256-gcm requires a 256‑bit key (32 bytes). Node’s crypto.createCipheriv / createDecipheriv must be given a 32‑byte Buffer (or equivalent KeyObject) for "aes-256-gcm" — otherwise it will fail or be rejected by OpenSSL. Use crypto.randomBytes(32) or derive a 32‑byte key (e.g. PBKDF2/HKDF) if you need a key from a password. Also note GCM best-practices: 12‑byte IV recommended and 16‑byte (128‑bit) auth tag default. [1][2][3]

Sources:

  • Node.js crypto docs (Cipheriv / key argument, authTag/IV guidance). [1]
  • NIST / standards stating AES key sizes (128/192/256) for GCM. [2]
  • RFC / practical examples and community guidance (key/IV/tag lengths, generation). [3]

References:
[1] Node.js Crypto documentation.
[2] NIST SP 800-38D / CSRC pages on GCM.
[3] RFCs and examples (RFC 5116, Node.js examples).


🏁 Script executed:

# Check if quiz-crypto.ts is properly guarded from client imports
# Look at how it's used in the quiz page - is it truly server-only?
cat frontend/app/[locale]/quiz/[slug]/page.tsx | head -50

Repository: DevLoversTeam/devlovers.net

Length of output: 1849


🏁 Script executed:

# Verify the actual key length requirement for AES-256-GCM
python3 << 'EOF'
# AES-256 means 256-bit key = 32 bytes
# In hex encoding, each byte is 2 hex characters
# So 32 bytes = 64 hex characters
key_bytes = 32
key_hex_chars = key_bytes * 2
print(f"AES-256-GCM requires: {key_bytes} bytes = {key_hex_chars} hex characters")
EOF

Repository: DevLoversTeam/devlovers.net

Length of output: 157


🏁 Script executed:

# Check if there are any tests for quiz-crypto to see how the key should be formatted
fd "quiz-crypto" --type f

Repository: DevLoversTeam/devlovers.net

Length of output: 103


🌐 Web query:

Next.js 16 server-only directive built-in

💡 Result:

Short answer: Next.js 16 does not add a new built‑in "server‑only" directive — it uses the separate npm package import 'server-only' to mark modules as server‑only, while the 'use server' directive remains the way to declare Server Actions/functions. See the Next.js docs. [1][2]

Sources:

  • Next.js v16 upgrade guide (mentions server‑only usage and 'use server'). [1]
  • Next.js rendering/composition docs (explains importing "server-only" to get a build‑time error if imported into client code). [2]

🏁 Script executed:

# Verify the quiz page is indeed a Server Component and not a Client Component
grep -n "use client" frontend/app/[locale]/quiz/[slug]/page.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

# Check if server-only is actually in dependencies or if it's only mocked
cat frontend/package.json | grep -C 5 "dependencies"

Repository: DevLoversTeam/devlovers.net

Length of output: 573


🏁 Script executed:

# Look at the mock to understand how server-only is being handled
cat frontend/lib/tests/__mocks__/server-only.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 211


🏁 Script executed:

# Verify if the package.json in the actual project root (not frontend/) has server-only
cat package.json | grep -i "server-only" 2>/dev/null || echo "Not found in root"

Repository: DevLoversTeam/devlovers.net

Length of output: 150


🏁 Script executed:

# Check if server-only is actually available as a built-in or via Next.js
# by looking at what happens when we try to import it
find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "server-only" | head -3 | xargs cat | grep -A 5 "server-only"

Repository: DevLoversTeam/devlovers.net

Length of output: 572


🏁 Script executed:

# Verify the complete dependencies list to be absolutely sure server-only isn't there
cat frontend/package.json | jq '.dependencies | keys[] | select(. | contains("server"))'

Repository: DevLoversTeam/devlovers.net

Length of output: 97


🏁 Script executed:

# Double-check that server-only isn't listed anywhere in package.json
cat frontend/package.json | grep -i "server"

Repository: DevLoversTeam/devlovers.net

Length of output: 112


🏁 Script executed:

# Check if there's an npm install or lockfile that might have server-only as transitive dependency
ls -la frontend/ | grep -E "package-lock|yarn.lock|pnpm-lock"

Repository: DevLoversTeam/devlovers.net

Length of output: 140


🏁 Script executed:

# Check if server-only is in package-lock.json (transitive dependency)
grep -i '"server-only"' frontend/package-lock.json | head -5

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

# Also verify what Next.js 16 actually provides - check node_modules or lock file for server-only inclusion
grep -A 5 '"name": "next"' frontend/package-lock.json | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

# Check Next.js documentation or check if server-only might be a built-in in 16
# by looking at imports in node_modules
find frontend/node_modules/next -name "server-only*" 2>/dev/null | head -5

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

# Verify by checking what other files that use server-only did to get it working
head -5 frontend/lib/auth.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 199


🏁 Script executed:

# Check Next.js 16 node_modules to see if server-only comes as transitive dependency
ls -la frontend/node_modules/ | grep -i server

Repository: DevLoversTeam/devlovers.net

Length of output: 140


🏁 Script executed:

# Check if there's a special handling in next.config or tsconfig for server-only
cat frontend/next.config.js 2>/dev/null || cat frontend/next.config.ts 2>/dev/null || echo "No config found"

Repository: DevLoversTeam/devlovers.net

Length of output: 1019


Add server-only marker and validate QUIZ_ENCRYPTION_KEY as 32-byte (64-char hex) for AES-256-GCM.

The module exports functions that use Node.js crypto and should never be bundled into client code. Validation is also missing: if the key isn't exactly 64 hex characters (32 bytes for AES-256-GCM), the error at runtime will be unclear. The pattern is already established elsewhere in the codebase.

Implementation
+import 'server-only';
 import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

 const ALGORITHM = 'aes-256-gcm';
 const IV_LENGTH = 16;
 const AUTH_TAG_LENGTH = 16;
+const KEY_LENGTH_BYTES = 32;
+const KEY_LENGTH_HEX = KEY_LENGTH_BYTES * 2;

 function getEncryptionKey(): Buffer {
   const key = process.env.QUIZ_ENCRYPTION_KEY;
   if (!key) {
     throw new Error('QUIZ_ENCRYPTION_KEY environment variable is not set');
   }
-  return Buffer.from(key, 'hex');
+  if (key.length !== KEY_LENGTH_HEX || !/^[0-9a-fA-F]+$/.test(key)) {
+    throw new Error(`QUIZ_ENCRYPTION_KEY must be ${KEY_LENGTH_HEX} hex chars (${KEY_LENGTH_BYTES} bytes)`);
+  }
+  const buf = Buffer.from(key, 'hex');
+  if (buf.length !== KEY_LENGTH_BYTES) {
+    throw new Error(`QUIZ_ENCRYPTION_KEY must decode to ${KEY_LENGTH_BYTES} bytes`);
+  }
+  return buf;
 }

This also applies to encryptAnswers(), decryptAnswers(), and createEncryptedAnswersBlob() which all depend on the validated key.

🤖 Prompt for AI Agents
In @frontend/lib/quiz/quiz-crypto.ts around lines 1 - 13, Add a server-only
marker at the top of quiz-crypto.ts (e.g., the established "server-only"
directive used in the repo) and harden getEncryptionKey(): ensure
process.env.QUIZ_ENCRYPTION_KEY exists, validate it matches exactly 64 hex chars
(or /^[0-9a-fA-F]{64}$/) and/or that Buffer.from(key,'hex').length === 32, and
throw a clear error if not; ensure encryptAnswers, decryptAnswers, and
createEncryptedAnswersBlob call the validated getEncryptionKey() so they never
operate with an invalid key.

Comment on lines +1 to +5
export const QUIZ_ALLOW_RESTORE_KEY = 'quiz-allow-restore';

export function getQuizReloadKey(quizId: string): string {
return `quiz-reload:${quizId}`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "quiz-allow-restore" --type=ts -g '!quiz-storage-keys.ts' frontend/

Repository: DevLoversTeam/devlovers.net

Length of output: 169


🏁 Script executed:

cat -n frontend/components/shared/LanguageSwitcher.tsx | head -25 | tail -15

Repository: DevLoversTeam/devlovers.net

Length of output: 671


Update LanguageSwitcher.tsx to use the new constant.

The constant QUIZ_ALLOW_RESTORE_KEY is created but not being used. The hardcoded 'quiz-allow-restore' string at frontend/components/shared/LanguageSwitcher.tsx:18 needs to import and use this constant instead. Without this change, the refactoring remains incomplete and the constant serves no purpose.

🤖 Prompt for AI Agents
In @frontend/lib/quiz/quiz-storage-keys.ts around lines 1 - 5, Replace the
hardcoded 'quiz-allow-restore' string in the LanguageSwitcher component with the
exported constant QUIZ_ALLOW_RESTORE_KEY: import QUIZ_ALLOW_RESTORE_KEY from the
quiz-storage-keys module and use that constant wherever the literal is used
(specifically the value used at the place corresponding to the previous literal
in the LanguageSwitcher component), removing the unused constant from being
idle; ensure imports compile and update any tests or usages that expect the
literal string to now reference QUIZ_ALLOW_RESTORE_KEY.

Comment thread frontend/messages/en.json
Comment on lines +154 to +159
"exitModal": {
"title": "Exit Quiz?",
"message": "Your progress will not be saved.",
"confirm": "Exit",
"cancel": "Continue"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent indentation in exitModal object.

The exitModal content uses 2-space indentation while the rest of the file consistently uses 4 spaces for nested objects. This should be aligned for consistency.

Suggested fix
     "exitModal": {
-  "title": "Exit Quiz?",
-  "message": "Your progress will not be saved.",
-  "confirm": "Exit",
-  "cancel": "Continue"
-}
+        "title": "Exit Quiz?",
+        "message": "Your progress will not be saved.",
+        "confirm": "Exit",
+        "cancel": "Continue"
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"exitModal": {
"title": "Exit Quiz?",
"message": "Your progress will not be saved.",
"confirm": "Exit",
"cancel": "Continue"
}
"exitModal": {
"title": "Exit Quiz?",
"message": "Your progress will not be saved.",
"confirm": "Exit",
"cancel": "Continue"
}
🤖 Prompt for AI Agents
In @frontend/messages/en.json around lines 154 - 159, The exitModal JSON object
uses 2-space indentation inconsistent with the rest of the file; update the
indentation for the "exitModal" block so nested lines (title, message, confirm,
cancel) use 4 spaces to match the file's style, ensuring the keys remain
unchanged and the object formatting aligns with the surrounding JSON entries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants